Einführung

Persolz API-Dokumentation

Verbinde Persolz mit Deinem Shop, Deinem Funnel oder Deinem CRM: Kontakte pflegen, Newsletter versenden, Seinfeld-Mails planen, Seifenopern starten – und über Webhooks sofort erfahren, was passiert.

  • 127 Endpunkte
  • 17 Webhook-Ereignisse
  • 120 Anfragen/Minute
  • v1.1.0 OpenAPI 3.1

So funktioniert die API

Die Persolz-API ist eine klassische REST-API: Jede Ressource (Kontakt, Liste, Newsletter …) hat eine eigene URL, Du arbeitest mit den HTTP-Methoden GET (lesen), POST (anlegen oder auslösen), PATCH (teilweise ändern), PUT (ersetzen) und DELETE (löschen). Daten gehen als JSON (UTF-8) hin und zurück.

  • Alles, was die App kann: Kontakte und Listen, E-Mails und Newsletter, Seinfeld-Mails, Seifenopern, Vorlagen, Anhänge, A/B-Tests, Termine, KI-Texte, Statistiken und Protokolle.
  • Verständliche Fehler: Jede Fehlerantwort hat einen festen Code und eine deutsche Meldung, die sagt, was zu tun ist.
  • Sicher: Schlüssel mit Berechtigungen, IP- und Origin-Freigaben, signierte Webhooks, Hosting in Deutschland.
  • Teil von Persolz Pro: Die API ist im Pro-Tarif enthalten – mit 120 Anfragen pro Minute je Schlüssel.

Alle Pfade in dieser Doku beziehen sich auf die Basis-URL https://persolz.com/api/v1. Den API-Index und die maschinenlesbare OpenAPI-Beschreibung erreichst Du ohne Schlüssel.

Basis-URL URL
https://persolz.com/api/v1
GET API-Index (ohne Schlüssel)
curl https://persolz.com/api/v1
<?php
$ch = curl_init('https://persolz.com/api/v1');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1');
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1",
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

Aufbau der Antworten

Erfolgreiche Antworten haben immer denselben Umschlag: Die eigentlichen Daten stehen in data, Zusatzinfos wie die Request-ID und die Seiteninfos in meta. Fehler kommen stattdessen als error-Objekt (siehe Fehler).

  • Statuscodes: 200 OK, 201 angelegt, 202 angenommen (Versand läuft), 204 erledigt ohne Inhalt (z. B. nach dem Löschen).
  • Zeitangaben: ISO 8601 mit Zeitzone Europe/Berlin, z. B. 2026-10-09T14:05:00+02:00. Eingaben dürfen auch 2026-10-09 14:05 oder 2026-10-09T14:05 sein.
  • IDs sind ganze Zahlen, Quoten (open_rate …) Werte zwischen 0 und 1.
  • Ignoriere unbekannte Felder: Neue Felder können jederzeit dazukommen – Dein Code sollte damit klarkommen.
Erfolg JSON
{
  "data": ["…"],
  "meta": {
    "request_id": "req_…",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 120,
      "pages": 5
    }
  }
}
Fehler JSON
{
  "error": {
    "code": "validation_error",
    "message": "Bitte gib eine E-Mail-Adresse an.",
    "fields": {
      "email": "Bitte gib eine E-Mail-Adresse an."
    },
    "request_id": "req_…"
  }
}

Anleitung

Schnellstart in 5 Minuten

Vom Schlüssel zum ersten versendeten Newsletter – Schritt für Schritt. Zu jedem Schritt gibt es fertigen Code in cURL, PHP, JavaScript und Python.

1. API-Schlüssel erstellen

Melde Dich in Persolz an und öffne API & Doku → API-Schlüssel. Klicke auf „Neuer API-Schlüssel“, gib ihm einen sprechenden Namen (z. B. „Shop“ oder „Funnel kartenherz.com“) und wähle die Berechtigungen.

Der Schlüssel beginnt mit pz_live_ und wird nur einmal vollständig angezeigt. Speichere ihn sicher – am besten als Umgebungsvariable PERSOLZ_API_KEY auf Deinem Server. In den Beispielen steht an seiner Stelle der Platzhalter pz_live_DEIN_API_SCHLUESSEL.

Schlüssel hinterlegen Shell
# Schlüssel als Umgebungsvariable ablegen (nie ins Git-Repository!)
export PERSOLZ_API_KEY="pz_live_DEIN_API_SCHLUESSEL"

# Verbindung testen – ohne Schlüssel kommt 401
curl https://persolz.com/api/v1/me -H "Authorization: Bearer $PERSOLZ_API_KEY"

2. Verbindung testen

Rufe GET /me auf. Die Antwort zeigt Dein Konto, Deinen Tarif, die Limits und den verwendeten Schlüssel. Bekommst Du 401, fehlt der Header oder der Schlüssel stimmt nicht; 402 heißt: Die API ist in Deinem Tarif nicht enthalten.

GET Konto abrufen
curl https://persolz.com/api/v1/me \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/me');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/me', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/me",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

3. Liste finden und ersten Kontakt anlegen

Mit GET /lists siehst Du Deine Listen samt ID. Dann legst Du mit POST /contacts einen Kontakt an – die Adresse ist der eindeutige Schlüssel: Gibt es sie schon, wird der Kontakt aktualisiert (Upsert, Status 200 statt 201). Tags werden ergänzt, Listen hinzugefügt.

Ist die Liste auf Double-Opt-in gestellt, bekommt der Kontakt automatisch eine Bestätigungsmail und bleibt bis zur Bestätigung pending.

GET Listen abrufen
curl https://persolz.com/api/v1/lists \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/lists",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
POST Kontakt anlegen
curl -X POST https://persolz.com/api/v1/contacts \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "list_ids": [3],
    "tags": ["api-test"]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'email' => 'anna@beispiel.de',
        'first_name' => 'Anna',
        'list_ids' => [3],
        'tags' => ['api-test'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    email: 'anna@beispiel.de',
    first_name: 'Anna',
    list_ids: [3],
    tags: ['api-test']
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/contacts",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "email": "anna@beispiel.de",
        "first_name": "Anna",
        "list_ids": [3],
        "tags": ["api-test"],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

4. Newsletter als Entwurf anlegen

Ein Newsletter entsteht immer zuerst als Entwurf – es wird noch nichts versendet. Inhalt kommt als html, als text oder aus einer Vorlage (template_key). Platzhalter wie {{vorname|Freund}} werden später je Empfänger ersetzt. Merke Dir die id aus der Antwort.

POST Entwurf anlegen
curl -X POST https://persolz.com/api/v1/newsletters \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "API-Test",
    "subject": "Hallo {{vorname|Freund}}, das ist mein erster API-Newsletter",
    "html": "<p>Hallo {{vorname|Freund}},</p><p>dieser Newsletter kommt direkt aus meinem Code.</p>",
    "list_ids": [3]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'name' => 'API-Test',
        'subject' => 'Hallo {{vorname|Freund}}, das ist mein erster API-Newsletter',
        'html' => '<p>Hallo {{vorname|Freund}},</p><p>dieser Newsletter kommt direkt aus meinem Code.</p>',
        'list_ids' => [3],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'API-Test',
    subject: 'Hallo {{vorname|Freund}}, das ist mein erster API-Newsletter',
    html: '<p>Hallo {{vorname|Freund}},</p><p>dieser Newsletter kommt direkt aus meinem Code.</p>',
    list_ids: [3]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/newsletters",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "name": "API-Test",
        "subject": "Hallo {{vorname|Freund}}, das ist mein erster API-Newsletter",
        "html": "<p>Hallo {{vorname|Freund}},</p><p>dieser Newsletter kommt direkt aus meinem Code.</p>",
        "list_ids": [3],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

5. Testmail an Dich senden

Prüfe den Newsletter, bevor er rausgeht: POST /newsletters/{id}/test schickt ihn mit „[TEST]“ im Betreff an bis zu fünf Adressen – ohne Tracking, personalisiert mit einem Beispielkontakt.

POST Testmail
curl -X POST https://persolz.com/api/v1/newsletters/812/test \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "du@beispiel.de"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/812/test');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => 'du@beispiel.de',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/812/test', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    to: 'du@beispiel.de'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/newsletters/812/test",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "to": "du@beispiel.de",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

6. Newsletter senden

Alles gut? Dann löst POST /newsletters/{id}/send den Versand aus. Persolz prüft Betreff, Inhalt, Empfänger und SMTP-Konto, verschickt einen ersten Schwung sofort und den Rest automatisch im Hintergrund. Die Antwort kommt mit 202 Accepted.

Lieber später? POST /newsletters/{id}/schedule mit scheduled_at plant den Versand. Geschafft! Als Nächstes lohnt sich ein Blick auf Webhooks und die Funnel-Integration.

POST Jetzt senden
curl -X POST https://persolz.com/api/v1/newsletters/812/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/812/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/812/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Idempotency-Key': crypto.randomUUID()
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/newsletters/812/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort JSON
{
  "data": {
    "id": 812,
    "status": "sending",
    "queued": 1250,
    "sent": 150,
    "failed": 0,
    "message": "Versand gestartet: 150 von 1.250 E-Mails sind raus, der Rest folgt automatisch."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Grundlagen

Authentifizierung

Jede Anfrage (außer den öffentlichen Endpunkten) braucht einen API-Schlüssel. Schlüssel erstellst, erneuerst und widerrufst Du unter API & Doku.

Schlüssel mitsenden

Sende den Schlüssel am besten als Bearer-Token im Header Authorization. Alternativ funktioniert der Header X-API-Key. Der Query-Parameter ?api_key= ist nur eine Notlösung, denn URLs landen in Server-Logs und im Browserverlauf (Persolz maskiert ihn im API-Protokoll).

VarianteBeispiel
Bearer (empfohlen)Authorization: Bearer pz_live_…
Eigener HeaderX-API-Key: pz_live_…
Query (nicht empfohlen)?api_key=pz_live_…

Ein Schlüssel besteht aus pz_live_ und 40 Zeichen (A–Z, a–z, 0–9). Fehlt er, ist er falsch oder widerrufen, antwortet die API mit 401 unauthorized.

Drei Wege cURL
# Empfohlen: Bearer-Token im Authorization-Header
curl https://persolz.com/api/v1/me \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"

# Alternative: eigener Header
curl https://persolz.com/api/v1/me \
  -H "X-API-Key: pz_live_DEIN_API_SCHLUESSEL"

# Nur im Notfall: als Query-Parameter (landet in Logs und im Verlauf)
curl "https://persolz.com/api/v1/me?api_key=pz_live_DEIN_API_SCHLUESSEL"
Antwort ohne gültigen Schlüssel (401) JSON
{
  "error": {
    "code": "unauthorized",
    "message": "Kein API-Schlüssel gefunden. Sende ihn im Header „Authorization: Bearer pz_live_…“ oder „X-API-Key“.",
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Berechtigungen (Scopes)

Gib jedem Schlüssel nur die Rechte, die er braucht. Ein Shop, der nur Kontakte einträgt, braucht write – aber kein send. Neue Schlüssel haben standardmäßig alle Rechte (*). Bei jedem Endpunkt in der Referenz steht, welcher Scope nötig ist. Fehlt er, kommt 403 forbidden mit reason: "insufficient_scope".

ScopeErlaubt
readLesenAlle GET-Endpunkte: Kontakte, Listen, Kampagnen, Statistiken …
writeSchreibenAnlegen, Ändern und Löschen (POST/PATCH/PUT/DELETE)
sendVersendenE-Mails, Newsletter und Seinfeld-Mails verschicken oder planen
aiKIKI-Texte erzeugen (verbraucht Dein KI-Guthaben)
*AlleVoller Zugriff (Standard für neue Schlüssel).
Fehlender Scope (403) JSON
{
  "error": {
    "code": "forbidden",
    "message": "Diesem API-Schlüssel fehlt die Berechtigung „Versenden“ (Scope „send“).",
    "reason": "insufficient_scope",
    "required_scope": "send",
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

IP-Adressen und Origins freigeben

Zusätzlich zu den Scopes kannst Du jeden Schlüssel einschränken:

  • Erlaubte IP-Adressen: einzelne Adressen (203.0.113.7) oder Bereiche (203.0.113.0/24). Ideal für Server mit fester IP – ein gestohlener Schlüssel nützt dann niemandem.
  • Erlaubte Origins: Websites, die den Schlüssel im Browser nutzen dürfen, z. B. https://kartenherz.com oder mit Platzhalter für Subdomains https://*.kartenherz.com. Mehr dazu unter CORS & Browser.

Anfragen von anderswo werden mit 403 forbidden abgelehnt (reason: ip_not_allowed bzw. origin_not_allowed).

Schlüssel sicher aufbewahren

  • Nie in öffentlichen Code: nicht ins HTML Deiner Website, nicht in eine App, nicht ins Git-Repository. Nutze Umgebungsvariablen oder einen Secret-Speicher.
  • Ein Schlüssel pro Anwendung: So siehst Du im API-Protokoll, wer was macht, und kannst gezielt sperren.
  • Erneuern statt teilen: „Erneuern“ erzeugt sofort einen neuen Schlüssel mit denselben Einstellungen – der alte ist ab dann ungültig.
  • Verdacht auf Missbrauch? Widerrufe den Schlüssel sofort unter API & Doku. Jede Anfrage wird mit Zeit, IP, Herkunft und Ergebnis protokolliert (90 Tage).

Grundlagen

Fehler

Persolz nutzt HTTP-Statuscodes: 2xx = Erfolg, 4xx = Problem mit der Anfrage (Eingaben, Rechte, Limits), 5xx = Problem bei Persolz. Jede Fehlerantwort enthält ein error-Objekt mit festem Code und deutscher Meldung.

Das Fehler-Objekt

FeldBedeutung
error.codeFester, maschinenlesbarer Code – darauf reagiert Dein Programm.
error.messageVerständliche Meldung auf Deutsch – kannst Du direkt anzeigen.
error.fieldsNur bei 422: Fehler je Eingabefeld, z. B. {"email": "…"}.
error.request_idID der Anfrage (auch im Header X-Request-Id). Gib sie bei Support-Anfragen an.
weitere FelderJe nach Fall z. B. reason, retry_after, upgrade_url, feature.

Werte immer zuerst den Statuscode und error.code aus – die Meldungstexte können sich verbessern.

Ungültige Eingabe (422) JSON
{
  "error": {
    "code": "validation_error",
    "message": "Bitte gib eine E-Mail-Adresse an.",
    "fields": {
      "email": "Bitte gib eine E-Mail-Adresse an."
    },
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}
Limit des Tarifs erreicht (402) JSON
{
  "error": {
    "code": "limit_reached",
    "message": "Du hast das Kontaktlimit Deines Tarifs erreicht (250). Mit Persolz Pro sind es unbegrenzt viele.",
    "upgrade_url": "https://persolz.com/billing",
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Alle Fehlercodes

Wie Du mit Fehlern sauber umgehst – inklusive fertiger Wiederhol-Logik – zeigt die Anleitung Fehlerbehandlung & Wiederholungen.

Status & CodeBedeutungWas tun?
400 bad_request Die Anfrage ist formal ungültig (z. B. kaputtes JSON). JSON prüfen (gültig? UTF-8?) und Content-Type: application/json senden.
401 unauthorized Kein, ein ungültiger oder ein widerrufener API-Schlüssel. Header Authorization: Bearer pz_live_… prüfen; Schlüssel ggf. neu erstellen.
402 plan_required Die Funktion (oder die API selbst) ist im aktuellen Tarif nicht enthalten. Funktion im Tarif freischalten (upgrade_url in der Antwort).
402 limit_reached Ein Limit des Tarifs ist erreicht (z. B. Kontakte, Listen, Tageslimit, Speicher). Tariflimit erhöhen oder Platz schaffen; Stand mit GET /plan/limits.
403 forbidden Fehlende Berechtigung (Scope), nicht erlaubte IP-Adresse oder Herkunft, gesperrtes Konto. reason auswerten: Scope, IP oder Origin beim Schlüssel freigeben.
404 not_found Die Ressource existiert nicht oder gehört nicht zu Deinem Konto. ID prüfen – Ressourcen anderer Konten sind nicht sichtbar.
405 method_not_allowed Die HTTP-Methode ist für diesen Pfad nicht erlaubt (siehe Header „Allow“). HTTP-Methode prüfen; erlaubte Methoden stehen im Header Allow.
409 conflict Konflikt mit dem aktuellen Zustand (z. B. bereits vorhanden, bereits versendet, Idempotency-Key wiederverwendet). Aktuellen Zustand abfragen; bei Idempotency-Konflikt neuen Key verwenden.
413 payload_too_large Die Anfrage oder Datei ist zu groß. Anfrage aufteilen (max. 1.000 Einträge) bzw. Datei verkleinern (max. 10 MB).
422 validation_error Eingaben sind ungültig. Details stehen je Feld in „error.fields“. error.fields auswerten und Eingaben korrigieren.
429 rate_limited Zu viele Anfragen. Warte die Sekunden aus „Retry-After“ ab. Retry-After abwarten, dann erneut senden.
500 server_error Unerwarteter Fehler. Bitte mit der Request-ID an den Support wenden. Mit Pause wiederholen; bleibt es dabei, Request-ID an den Support.
503 service_unavailable Ein abhängiger Dienst (z. B. KI-Anbieter) ist gerade nicht erreichbar. Mit Pause wiederholen (z. B. KI-Anbieter kurz nicht erreichbar).

Grundlagen

Rate-Limits

Damit die API für alle schnell bleibt, gilt ein Limit von 120 Anfragen pro Minute je API-Schlüssel. Brauchst Du mehr Durchsatz, verteile die Last auf mehrere Schlüssel – oder nutze die Massen-Endpunkte (z. B. POST /contacts/bulk). Für einzelne Schlüssel kannst Du im Entwickler-Portal ein niedrigeres eigenes Limit setzen (z. B. für ein Skript, das nie mehr als 30 Anfragen pro Minute braucht); X-RateLimit-Limit zeigt immer das Limit des verwendeten Schlüssels.

Header in jeder Antwort

Jede authentifizierte Antwort sagt Dir, wie viel Luft Du noch hast. Ist das Limit erreicht, antwortet die API mit 429 rate_limited und dem Header Retry-After (Sekunden). Warte diese Zeit ab und versuche es dann erneut – nicht sofort in einer Schleife.

HeaderBedeutung
X-RateLimit-LimitErlaubte Anfragen pro Minute für diesen Schlüssel.
X-RateLimit-RemainingVerbleibende Anfragen im aktuellen Fenster.
X-RateLimit-ResetUnix-Zeitstempel, zu dem das Fenster zurückgesetzt wird.
Retry-AfterNur bei 429: Sekunden bis zum nächsten Versuch.
Antwort-Header HTTP
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Request-Id: req_2b9f1c7d0e3a4b5c6d7e
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1791560735
Limit erreicht (429) JSON
{
  "error": {
    "code": "rate_limited",
    "message": "Zu viele Anfragen: Dieser API-Schlüssel darf 120 Anfragen pro Minute stellen. Bitte warte 23 Sekunden.",
    "retry_after": 23,
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Weitere Grenzen

BereichGrenze
Öffentliche Anmeldung (/subscribe/{token})10 Anmeldungen pro Minute je IP, 200 pro Tag je Liste
KI-Texte (POST /ai/generate)30 Anfragen pro Minute; alle KI-Endpunkte verbrauchen KI-Guthaben
Massenabgleich & Listen-Importmax. 1.000 Kontakte pro Anfrage
Seitengröße (per_page)max. 100 Einträge
Anhängemax. 10 MB je Datei, Speicher laut Tarif
E-Mail-Inhalt (html)max. 2 MB
Webhooks / API-Schlüsselmax. 20 Webhooks, 25 aktive Schlüssel

Dazu kommen die Limits Deines Tarifs (Kontakte, Listen, Versände pro Tag …). Wird eines erreicht, antwortet die API mit 402 limit_reached. Deinen aktuellen Stand zeigt GET /plan/limits.

Grundlagen

Paginierung

Listen-Endpunkte (z. B. GET /contacts) liefern ihre Ergebnisse seitenweise.

Seiten abrufen

ParameterBedeutung
pageSeite, ab 1 (Standard 1)
per_pageEinträge pro Seite, 1–100 (Standard 25)

Die Antwort enthält in meta.pagination die aktuelle Seite, die Seitengröße, die Gesamtzahl (total) und die Anzahl Seiten (pages). Frage so lange die nächste Seite ab, bis page gleich pages ist.

Tipp für Synchronisationen: Statt immer alles zu laden, holst Du mit updated_after=<Zeitpunkt des letzten Abgleichs>&sort=updated_asc nur geänderte Kontakte.

Alle Seiten laden
<?php
// Alle Kontakte einer Liste seitenweise abrufen
$apiKey = getenv('PERSOLZ_API_KEY');
$page = 1;
do {
    $query = http_build_query(['list_id' => 3, 'per_page' => 100, 'page' => $page]);
    $ch = curl_init('https://persolz.com/api/v1/contacts?' . $query);
    curl_setopt_array($ch, [
        CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 30,
    ]);
    $json = json_decode((string) curl_exec($ch), true);
    curl_close($ch);

    foreach ($json['data'] as $contact) {
        echo $contact['email'], PHP_EOL;
    }
    $pages = $json['meta']['pagination']['pages'];
    $page++;
} while ($page <= $pages);
// Alle Kontakte einer Liste seitenweise abrufen (Node.js 18+)
const headers = { 'Authorization': `Bearer ${process.env.PERSOLZ_API_KEY}` };
let page = 1;
let pages = 1;
do {
  const params = new URLSearchParams({ list_id: '3', per_page: '100', page: String(page) });
  const response = await fetch('https://persolz.com/api/v1/contacts?' + params, { headers });
  const json = await response.json();
  json.data.forEach((contact) => console.log(contact.email));
  pages = json.meta.pagination.pages;
  page++;
} while (page <= pages);
import os
import requests

headers = {"Authorization": f"Bearer {os.environ['PERSOLZ_API_KEY']}"}
page, pages = 1, 1
while page <= pages:
    result = requests.get(
        "https://persolz.com/api/v1/contacts",
        headers=headers,
        params={"list_id": 3, "per_page": 100, "page": page},
        timeout=30,
    ).json()
    for contact in result["data"]:
        print(contact["email"])
    pages = result["meta"]["pagination"]["pages"]
    page += 1
Antwort (gekürzt) JSON
{
  "data": [
    {
      "id": 42,
      "email": "anna@beispiel.de",
      "…": "…"
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "per_page": 100,
      "total": 1250,
      "pages": 13
    },
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Grundlagen

Idempotenz

Netzwerke sind unzuverlässig: Eine Anfrage kommt an, aber die Antwort geht verloren. Mit einem Idempotency-Key kannst Du POST-Anfragen gefahrlos wiederholen – ohne doppelte Kontakte oder doppelt versendete Mails.

So funktioniert es

  1. Erzeuge pro Vorgang einen eindeutigen Wert (z. B. eine UUID oder bestellung-4711-bestaetigung) und sende ihn im Header Idempotency-Key mit.
  2. Wiederholst Du die Anfrage innerhalb von 24 Stunden mit demselben Schlüssel, bekommst Du die gespeicherte Antwort zurück – mit dem Header Idempotent-Replayed: true. Es wird nichts doppelt angelegt oder versendet.
  3. Derselbe Schlüssel mit anderem Inhalt ergibt 409 conflict (reason: "idempotency_key_reused"). Verglichen wird der exakte Body – sende eine Wiederholung also byte-gleich (denselben JSON-String, nicht neu formatiert). Läuft die erste Anfrage noch, kommt ebenfalls 409 mit Retry-After: 2.

Idempotency-Keys funktionieren für alle POST-Endpunkte (außer den öffentlichen). Besonders wichtig sind sie beim Versenden und Anlegen. Server-Fehler (5xx) werden nicht gespeichert – ein neuer Versuch wird also wirklich neu ausgeführt.

POST E-Mail mit Idempotency-Key senden
curl -X POST https://persolz.com/api/v1/emails/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: bestellung-4711-bestaetigung" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "anna@beispiel.de",
    "subject": "Deine Bestellung #4711",
    "html": "<p>Danke für Deine Bestellung!</p>"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/emails/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: bestellung-4711-bestaetigung',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => 'anna@beispiel.de',
        'subject' => 'Deine Bestellung #4711',
        'html' => '<p>Danke für Deine Bestellung!</p>',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/emails/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'bestellung-4711-bestaetigung'
  },
  body: JSON.stringify({
    to: 'anna@beispiel.de',
    subject: 'Deine Bestellung #4711',
    html: '<p>Danke für Deine Bestellung!</p>'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/emails/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": "bestellung-4711-bestaetigung",
    },
    json={
        "to": "anna@beispiel.de",
        "subject": "Deine Bestellung #4711",
        "html": "<p>Danke für Deine Bestellung!</p>",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Wiederholung HTTP
# Zweiter Aufruf mit demselben Idempotency-Key:
HTTP/1.1 202 Accepted
Idempotent-Replayed: true
Content-Type: application/json; charset=utf-8

{"data":{"campaign_id":813,"status":"sending", …}}

Grundlagen

CORS & Browser

Die API lässt sich auch direkt aus dem Browser aufrufen. Dafür gelten besondere Regeln – vor allem: Ein Schlüssel im Browser ist für jeden sichtbar.

Wann ist der Browser okay?

Goldene Regel: Lege niemals einen Schlüssel mit vollen Rechten in eine öffentliche Seite. Jeder Besucher kann ihn im Quelltext lesen.

Du willst …Dann nutze …
ein Anmeldeformular auf Deiner Websiteden öffentlichen Anmelde-Endpunkt POST /subscribe/{token} – ganz ohne Schlüssel (Anleitung)
Kontakte mit Seifenoper, Tags und eigenen Feldern eintragenDeinen Server (PHP, Node …) – der Schlüssel bleibt geheim (Anleitung)
die API in einem geschützten Bereich nutzen (Mitgliederbereich, internes Dashboard)einen eingeschränkten Schlüssel mit erlaubter Origin und minimalem Scope (Anleitung)

Die Origin-Freigabe sorgt dafür, dass fremde Websites Deinen Schlüssel nicht im Browser ihrer Besucher verwenden können. Sie macht den Schlüssel aber nicht geheim: Außerhalb des Browsers (z. B. mit cURL) lässt sich der Origin-Header beliebig setzen. Gib Browser-Schlüsseln deshalb nur Rechte, deren Missbrauch keinen großen Schaden anrichtet – und erneuere sie regelmäßig.

Technische Details

  • Erlaubte Origins trägst Du beim Schlüssel ein, z. B. https://kartenherz.com und https://*.kartenherz.com (alle Subdomains). Ohne Eintrag sind alle Herkünfte erlaubt.
  • Preflight: Browser fragen vor Anfragen mit Authorization-Header per OPTIONS nach. Persolz antwortet mit 204, Access-Control-Max-Age: 600 (10 Minuten Cache) und Vary: Origin.
  • Lesbare Header: X-Request-Id, X-RateLimit-*, Retry-After und Idempotent-Replayed sind für JavaScript freigegeben.
  • Eine nicht freigegebene Herkunft bekommt 403 forbidden mit reason: "origin_not_allowed".
  • Der öffentliche Anmelde-Endpunkt antwortet immer mit Access-Control-Allow-Origin: *.
Preflight prüfen cURL
# So prüft der Browser vor einer Anfrage, ob er darf (Preflight)
curl -i -X OPTIONS https://persolz.com/api/v1/contacts \
  -H "Origin: https://kartenherz.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization, content-type"

# Antwort (gekürzt)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://kartenherz.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-API-Key, Idempotency-Key, X-Requested-With
Access-Control-Max-Age: 600
Vary: Origin

Grundlagen

Platzhalter

Mit Platzhaltern personalisierst Du Betreff, Vorschautext, Inhalt und sogar Links – für jeden Empfänger einzeln. Sie funktionieren in E-Mails, Newslettern, Seinfeld-Mails und Seifenopern.

Verfügbare Platzhalter

Schreibe Platzhalter in doppelte geschweifte Klammern, z. B. {{vorname}}. Groß-/Kleinschreibung und Leerzeichen sind egal ({{ Vorname }} geht auch). Zu jedem deutschen Platzhalter gibt es einen englischen Alias.

  • Ersatzwert: {{vorname|Freund}} – ist der Vorname leer, steht dort „Freund“.
  • Eigene Felder: Jeder Schlüssel aus custom_fields eines Kontakts ist ein Platzhalter, z. B. {{kundennummer}}.
  • Abmeldelink: {{abmelde_link}} setzt den persönlichen Abmeldelink ein. Fehlt er, ergänzt Persolz automatisch eine Abmeldezeile.
PlatzhalterBedeutungBeispiel
{{vorname}}{{first_name}} Vorname Max
{{nachname}}{{last_name}} Nachname Mustermann
{{name}}{{full_name}} Vollständiger Name Max Mustermann
{{email}} E-Mail-Adresse max.mustermann@example.com
{{adresse}}{{address}} Adresse (Straße & Hausnummer) Musterstraße 12
{{plz}}{{zip}} PLZ 10115
{{ort}}{{city}} Ort Berlin
{{telefon}}{{phone}} Telefon +49 30 1234567
{{notiz}}{{note}} Notiz Beispielkontakt für die Vorschau
{{datum}}{{date}} Heutiges Datum 15.10.2026
{{abmelde_link}}{{unsubscribe_url}} Abmeldelink https://persolz.com/u/…
Beispiel Vorher → nachher
Betreff:  Deine Bestellung #{{bestellnummer}}, {{vorname|Freund}}
HTML:     <p>Hallo {{vorname|Freund}},</p>
          <p>Dein Kartenset ist unterwegs nach {{ort}}.</p>
          <p><a href="https://kartenherz.com/konto?kd={{kundennummer}}">Zum Kundenkonto</a></p>

→ an Anna Schmidt (Berlin, custom_fields.kundennummer = K-1001), placeholders = {"bestellnummer": "4711"}

Betreff:  Deine Bestellung #4711, Anna
HTML:     <p>Hallo Anna,</p>
          <p>Dein Kartenset ist unterwegs nach Berlin.</p>
          <p><a href="https://kartenherz.com/konto?kd=K-1001">Zum Kundenkonto</a></p>

Eigene Platzhalter per API

Bei POST /emails/send kannst Du zusätzlich ein Objekt placeholders mitsenden, z. B. {"bestellnummer": "4711"} – dann wird {{bestellnummer}} für alle Empfänger dieser Anfrage gleich ersetzt. Brauchst Du je Empfänger andere Werte (Bestellnummer, Liefertermin), sende eine Anfrage pro Empfänger – siehe Transaktionale E-Mails.

Gut zu wissen:

  • Namen: a–z, 0–9, _ und -, max. 50 eigene Platzhalter pro Anfrage. Eingebaute Namen (vorname, email …) werden immer je Empfänger befüllt und lassen sich nicht überschreiben.
  • Werte werden im HTML automatisch maskiert – HTML-Code in Werten erscheint als Text. In Links werden Werte URL-kodiert.
  • Unbekannte Platzhalter bleiben stehen (z. B. Tippfehler wie {{vorame}}). Schicke vor großen Versänden eine Testmail.
POST E-Mail mit eigenem Platzhalter
curl -X POST https://persolz.com/api/v1/emails/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["anna@beispiel.de"],
    "subject": "Deine Bestellung #{{bestellnummer}}",
    "html": "<p>Hallo {{vorname|Freund}},</p><p>Deine Bestellung {{bestellnummer}} ist unterwegs.</p>",
    "placeholders": {
      "bestellnummer": "4711"
    }
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/emails/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => ['anna@beispiel.de'],
        'subject' => 'Deine Bestellung #{{bestellnummer}}',
        'html' => '<p>Hallo {{vorname|Freund}},</p><p>Deine Bestellung {{bestellnummer}} ist unterwegs.</p>',
        'placeholders' => [
            'bestellnummer' => '4711',
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/emails/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    to: ['anna@beispiel.de'],
    subject: 'Deine Bestellung #{{bestellnummer}}',
    html: '<p>Hallo {{vorname|Freund}},</p><p>Deine Bestellung {{bestellnummer}} ist unterwegs.</p>',
    placeholders: {
      bestellnummer: '4711'
    }
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/emails/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "to": ["anna@beispiel.de"],
        "subject": "Deine Bestellung #{{bestellnummer}}",
        "html": "<p>Hallo {{vorname|Freund}},</p><p>Deine Bestellung {{bestellnummer}} ist unterwegs.</p>",
        "placeholders": {
            "bestellnummer": "4711",
        },
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

Ereignisse in Echtzeit

Webhooks

Statt ständig nachzufragen, ob sich etwas getan hat, schickt Persolz Deinem System eine Nachricht, sobald etwas passiert: neuer Kontakt, Abmeldung, Öffnung, Klick, abgeschlossener Versand …

So funktionieren Webhooks

  1. Lege unter API & Doku → Webhooks oder per POST /webhooks einen Webhook an: die öffentlich erreichbare URL Deines Endpunkts (am besten https://) und die gewünschten Ereignisse (oder ["*"] für alle). Du bekommst ein Signatur-Secret (whsec_…).
  2. Bei jedem Ereignis sendet Persolz einen POST mit JSON an Deine URL: {"id", "event", "created_at", "data"}.
  3. Dein Endpunkt antwortet innerhalb von 4 Sekunden mit einem 2xx-Status. Alles andere gilt als Fehlschlag.
  4. Fehlschläge werden nach 1, 5, 30, 120 und 720 Minuten wiederholt (max. 6 Versuche). Nach 20 Fehlschlägen in Folge wird der Webhook pausiert und Du bekommst eine Benachrichtigung.

Alle Zustellungen (mit Payload, Antwortcode und Fehler) siehst Du 30 Tage lang im Zustellprotokoll – dort kannst Du sie auch erneut senden. Webhook-URLs müssen öffentlich erreichbar sein; interne Adressen (localhost, private Netze) werden aus Sicherheitsgründen nicht beliefert.

HeaderInhalt
X-Persolz-EventName des Ereignisses, z. B. contact.created
X-Persolz-DeliveryID der Zustellung (bei Wiederholungen gleich)
X-Persolz-Event-IdID des Ereignisses (evt_…) – nutze sie zum Entdoppeln
X-Persolz-Signaturet=<Unix-Zeit>,v1=<HMAC-SHA256 als Hex>
POST Webhook anlegen
curl -X POST https://persolz.com/api/v1/webhooks \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://kartenherz.com/hooks/persolz",
    "events": ["contact.created", "contact.unsubscribed", "email.clicked"],
    "description": "Shop-Synchronisation"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'url' => 'https://kartenherz.com/hooks/persolz',
        'events' => ['contact.created', 'contact.unsubscribed', 'email.clicked'],
        'description' => 'Shop-Synchronisation',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://kartenherz.com/hooks/persolz',
    events: ['contact.created', 'contact.unsubscribed', 'email.clicked'],
    description: 'Shop-Synchronisation'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/webhooks",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "url": "https://kartenherz.com/hooks/persolz",
        "events": ["contact.created", "contact.unsubscribed", "email.clicked"],
        "description": "Shop-Synchronisation",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
So kommt ein Ereignis bei Dir an HTTP
POST /hooks/persolz HTTP/1.1
Host: kartenherz.com
Content-Type: application/json; charset=utf-8
User-Agent: Persolz-Webhooks/1.0
X-Persolz-Event: contact.created
X-Persolz-Delivery: 9001
X-Persolz-Event-Id: evt_3f9a1c2b4d5e6f708192a3b4
X-Persolz-Signature: t=1791560735,v1=5f3a8c1e9b2d7f6a4c0e8b1d3f5a7c9e2b4d6f8a0c1e3b5d7f9a2c4e6b8d0f1a

{"id":"evt_3f9a1c2b4d5e6f708192a3b4","event":"contact.created","created_at":"2026-10-09T14:05:00+02:00","data":{"contact":{"id":42,"email":"anna@beispiel.de", …}}}

Ereignisse

Diese Ereignisse kannst Du abonnieren. Jedes kommt im gleichen Umschlag; was in data steht, zeigt das Beispiel des jeweiligen Ereignisses. email.opened kommt nur bei der ersten Öffnung, email.clicked beim ersten Klick je Link – Bots werden herausgefiltert. Die Reihenfolge ist nicht garantiert (bei neuen Kontakten kommt z. B. list.joined oft vor contact.created).

contact.created Kontakt angelegt Ein neuer Kontakt wurde angelegt (App, Import, API, Formular, Integration).
Felder in data
FeldBeschreibung
contact Kontakt-Objekt Der Kontakt. Kontakt-Objekt
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "contact.created",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "contact": {
      "id": 42,
      "email": "anna@beispiel.de",
      "first_name": "Anna",
      "last_name": "Schmidt",
      "address": "Lindenweg 3",
      "zip": "10115",
      "city": "Berlin",
      "phone": "+49 30 1234567",
      "note": "Kam über den Kartenherz-Funnel",
      "tags": ["funnel", "kartenherz"],
      "status": "subscribed",
      "source": "api",
      "custom_fields": {
        "kundennummer": "K-1001"
      },
      "doi_confirmed_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00",
      "lists": [
        {
          "id": 3,
          "name": "Kartenherz Funnel",
          "color": "#5B3DF5"
        }
      ]
    }
  }
}
contact.updated Kontakt geändert Daten, Status oder Listen eines Kontakts haben sich geändert (auch nach Double-Opt-in-Bestätigung).
Felder in data
FeldBeschreibung
contact Kontakt-Objekt Der Kontakt. Kontakt-Objekt
changed array<string> Namen der geänderten Felder.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "contact.updated",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "contact": {
      "id": 42,
      "email": "anna@beispiel.de",
      "first_name": "Anna",
      "last_name": "Schmidt",
      "address": "Lindenweg 3",
      "zip": "10115",
      "city": "Berlin",
      "phone": "+49 30 1234567",
      "note": "Kam über den Kartenherz-Funnel",
      "tags": ["funnel", "kartenherz"],
      "status": "subscribed",
      "source": "api",
      "custom_fields": {
        "kundennummer": "K-1001"
      },
      "doi_confirmed_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00",
      "lists": [
        {
          "id": 3,
          "name": "Kartenherz Funnel",
          "color": "#5B3DF5"
        }
      ]
    },
    "changed": ["first_name", "lists"]
  }
}
contact.deleted Kontakt gelöscht Ein Kontakt wurde gelöscht.
Felder in data
FeldBeschreibung
contact Kontakt-Objekt Der Kontakt. Kontakt-Objekt
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "contact.deleted",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "contact": {
      "id": 42,
      "email": "anna@beispiel.de",
      "first_name": "Anna",
      "last_name": "Schmidt",
      "address": "Lindenweg 3",
      "zip": "10115",
      "city": "Berlin",
      "phone": "+49 30 1234567",
      "note": "Kam über den Kartenherz-Funnel",
      "tags": ["funnel", "kartenherz"],
      "status": "subscribed",
      "source": "api",
      "custom_fields": {
        "kundennummer": "K-1001"
      },
      "doi_confirmed_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00",
      "lists": [
        {
          "id": 3,
          "name": "Kartenherz Funnel",
          "color": "#5B3DF5"
        }
      ]
    }
  }
}
contact.unsubscribed Kontakt abgemeldet Ein Kontakt hat sich abgemeldet (Abmeldelink, Ein-Klick-Abmeldung, manuell oder per API).
Felder in data
FeldBeschreibung
contact Kontakt-Objekt Der Kontakt. Kontakt-Objekt
email string E-Mail-Adresse des Kontakts.
source string Quelle bzw. Auslöser (bei inbox.replied: user, ai oder rule).
reason string · null Grund (optional).
campaign_id integer ID der Kampagne.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "contact.unsubscribed",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "contact": {
      "status": "unsubscribed",
      "id": 42,
      "email": "anna@beispiel.de",
      "first_name": "Anna",
      "last_name": "Schmidt",
      "address": "Lindenweg 3",
      "zip": "10115",
      "city": "Berlin",
      "phone": "+49 30 1234567",
      "note": "Kam über den Kartenherz-Funnel",
      "tags": ["funnel", "kartenherz"],
      "source": "api",
      "custom_fields": {
        "kundennummer": "K-1001"
      },
      "doi_confirmed_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00",
      "lists": [
        {
          "id": 3,
          "name": "Kartenherz Funnel",
          "color": "#5B3DF5"
        }
      ]
    },
    "email": "anna@beispiel.de",
    "source": "link",
    "reason": null,
    "campaign_id": 812
  }
}
list.joined Liste beigetreten Ein aktiver Kontakt wurde in eine Liste aufgenommen.
Felder in data
FeldBeschreibung
list object Die Liste.
list.id integer ID der Liste.
list.name string Name der Liste.
contact Kontakt-Objekt Der Kontakt. Kontakt-Objekt
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "list.joined",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "list": {
      "id": 3,
      "name": "Kartenherz Funnel"
    },
    "contact": {
      "id": 42,
      "email": "anna@beispiel.de",
      "first_name": "Anna",
      "last_name": "Schmidt",
      "address": "Lindenweg 3",
      "zip": "10115",
      "city": "Berlin",
      "phone": "+49 30 1234567",
      "note": "Kam über den Kartenherz-Funnel",
      "tags": ["funnel", "kartenherz"],
      "status": "subscribed",
      "source": "api",
      "custom_fields": {
        "kundennummer": "K-1001"
      },
      "doi_confirmed_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00",
      "lists": [
        {
          "id": 3,
          "name": "Kartenherz Funnel",
          "color": "#5B3DF5"
        }
      ]
    }
  }
}
email.opened E-Mail geöffnet Ein Empfänger hat eine E-Mail zum ersten Mal geöffnet (Bots werden herausgefiltert).
Felder in data
FeldBeschreibung
campaign object Die Kampagne der E-Mail.
campaign.id integer ID der Kampagne.
campaign.type string Versandart: email, newsletter, seinfeld oder soap.
campaign.name string Interner Name der Kampagne.
campaign.subject string Betreff.
campaign.ref_id integer · null Bei Seinfeld-Mails die ID der Seinfeld-Mail, bei Seifenopern die ID des Tages, sonst null.
contact_id integer ID des Kontakts.
email string E-Mail-Adresse des Kontakts.
variant string · null A/B-Variante (A oder B) oder null.
message_id string ID der versendeten Nachricht.
device string Gerät: desktop, mobile oder tablet.
implied boolean true, wenn die Öffnung aus einem Klick abgeleitet wurde (Bilder waren blockiert).
opened_at string (Datum/Zeit) Zeitpunkt der ersten Öffnung.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "email.opened",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "campaign": {
      "id": 812,
      "type": "newsletter",
      "name": "Herbst-Aktion",
      "subject": "Anna, das hier ist nur für Dich",
      "ref_id": null
    },
    "contact_id": 42,
    "email": "anna@beispiel.de",
    "variant": null,
    "message_id": "6f1c0d9a2b7e4c1f8a3d5e6f7a8b9c0d",
    "device": "mobile",
    "implied": false,
    "opened_at": "2026-10-09T14:05:00+02:00"
  }
}
email.clicked Link geklickt Ein Empfänger hat einen Link zum ersten Mal geklickt (je Link, Bots werden herausgefiltert).
Felder in data
FeldBeschreibung
campaign object Die Kampagne der E-Mail.
campaign.id integer ID der Kampagne.
campaign.type string Versandart: email, newsletter, seinfeld oder soap.
campaign.name string Interner Name der Kampagne.
campaign.subject string Betreff.
campaign.ref_id integer · null Bei Seinfeld-Mails die ID der Seinfeld-Mail, bei Seifenopern die ID des Tages, sonst null.
contact_id integer ID des Kontakts.
email string E-Mail-Adresse des Kontakts.
variant string A/B-Variante (A oder B) oder null.
message_id string ID der versendeten Nachricht.
link object Der geklickte Link.
link.id integer ID des Links.
link.url string Ziel-URL.
link.label string Linktext.
device string Gerät: desktop, mobile oder tablet.
clicked_at string (Datum/Zeit) Zeitpunkt des ersten Klicks auf diesen Link.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "email.clicked",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "campaign": {
      "id": 812,
      "type": "newsletter",
      "name": "Herbst-Aktion",
      "subject": "Anna, das hier ist nur für Dich",
      "ref_id": null
    },
    "contact_id": 42,
    "email": "anna@beispiel.de",
    "variant": "B",
    "message_id": "6f1c0d9a2b7e4c1f8a3d5e6f7a8b9c0d",
    "link": {
      "id": 77,
      "url": "https://kartenherz.com/angebot",
      "label": "Jetzt ansehen"
    },
    "device": "desktop",
    "clicked_at": "2026-10-09T14:06:12+02:00"
  }
}
campaign.sent Kampagne versendet Ein Newsletter, eine E-Mail oder eine Seinfeld-Mail wurde vollständig versendet.
Felder in data
FeldBeschreibung
campaign_id integer ID der Kampagne.
type string Versandart: email, newsletter oder seinfeld.
name string Interner Name der Kampagne.
subject string Betreff.
status string Status der Kampagne nach Abschluss (in der Regel sent).
recipients integer Empfänger gesamt.
sent integer Erfolgreich versendet.
failed integer Fehlgeschlagen.
started_at string (Datum/Zeit) Beginn des Versands oder null.
finished_at string (Datum/Zeit) Ende des Versands oder null.
ref_id integer · null Bei Seinfeld-Mails die ID der Seinfeld-Mail, bei Seifenopern die ID des Tages, sonst null.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "campaign.sent",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "campaign_id": 812,
    "type": "newsletter",
    "name": "Herbst-Aktion",
    "subject": "Anna, das hier ist nur für Dich",
    "status": "sent",
    "recipients": 1250,
    "sent": 1248,
    "failed": 2,
    "started_at": "2026-10-09T09:00:00+02:00",
    "finished_at": "2026-10-09T09:04:31+02:00",
    "ref_id": null
  }
}
soap.enrolled Seifenoper gestartet Ein Kontakt wurde in eine Seifenoper aufgenommen.
Felder in data
FeldBeschreibung
enrollment_id integer ID der Teilnahme an der Seifenoper.
sequence_id integer ID der Seifenoper.
sequence_name string Name der Seifenoper.
contact_id integer ID des Kontakts.
email string E-Mail-Adresse des Kontakts.
source string Quelle bzw. Auslöser (bei inbox.replied: user, ai oder rule).
start_date string (Datum) Startdatum der Teilnahme.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "soap.enrolled",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "enrollment_id": 501,
    "sequence_id": 7,
    "sequence_name": "Willkommen bei Kartenherz",
    "contact_id": 42,
    "email": "anna@beispiel.de",
    "source": "api",
    "start_date": "2026-10-10"
  }
}
soap.completed Seifenoper abgeschlossen Ein Kontakt hat alle Mails einer Seifenoper erhalten.
Felder in data
FeldBeschreibung
enrollment_id integer ID der Teilnahme an der Seifenoper.
sequence_id integer ID der Seifenoper.
sequence_name string Name der Seifenoper.
contact_id integer ID des Kontakts.
email string E-Mail-Adresse des Kontakts.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "soap.completed",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "enrollment_id": 501,
    "sequence_id": 7,
    "sequence_name": "Willkommen bei Kartenherz",
    "contact_id": 42,
    "email": "anna@beispiel.de"
  }
}
appointment.created Termin angelegt Ein Termin wurde im Terminbuch angelegt.

data ist ein Termin-Objekt.

Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "appointment.created",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "id": 55,
    "title": "Erstgespräch Anna Schmidt",
    "description": "Kennenlernen per Video",
    "location": "Zoom",
    "start_at": "2026-10-14T10:00:00+02:00",
    "end_at": "2026-10-14T10:30:00+02:00",
    "all_day": false,
    "contact_id": 42,
    "contact": {
      "id": 42,
      "name": "Anna Schmidt",
      "email": "anna@beispiel.de",
      "phone": "+49 30 1234567"
    },
    "attendees": ["anna@beispiel.de"],
    "phone": "",
    "color": "violet",
    "status": "planned",
    "status_label": "Geplant",
    "reminder_minutes": 60,
    "remind_attendees": true,
    "source": "api",
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00"
  }
}
appointment.updated Termin geändert Ein Termin wurde geändert, verschoben oder abgesagt.

data ist ein Termin-Objekt, zusätzlich:

Felder in data
FeldBeschreibung
changed array<string> Namen der geänderten Felder.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "appointment.updated",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "id": 55,
    "title": "Erstgespräch Anna Schmidt",
    "description": "Kennenlernen per Video",
    "location": "Zoom",
    "start_at": "2026-10-14T10:00:00+02:00",
    "end_at": "2026-10-14T10:30:00+02:00",
    "all_day": false,
    "contact_id": 42,
    "contact": {
      "id": 42,
      "name": "Anna Schmidt",
      "email": "anna@beispiel.de",
      "phone": "+49 30 1234567"
    },
    "attendees": ["anna@beispiel.de"],
    "phone": "",
    "color": "violet",
    "status": "planned",
    "status_label": "Geplant",
    "reminder_minutes": 60,
    "remind_attendees": true,
    "source": "api",
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "changed": ["start_at", "end_at"]
  }
}
inbox.received Neue Mail im Posteingang In einem verbundenen Postfach ist eine neue Mail eingegangen (ohne Newsletter, Abwesenheitsnotizen, Unzustellbarkeitsmeldungen und Spam-Verdacht).
Felder in data
FeldBeschreibung
message Mail-Objekt (Posteingang) Testnachricht (test.ping) bzw. die Mail im Posteingang (inbox.*). Mail-Objekt (Posteingang)
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "inbox.received",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "message": {
      "id": 9120,
      "mailbox_id": 2,
      "mailbox_email": "hallo@kartenherz.com",
      "folder": "Posteingang",
      "folder_role": "inbox",
      "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
      "message_id": "<CAF-anfrage-4711@mail.beispiel.de>",
      "direction": "in",
      "from": {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      },
      "to": [
        {
          "email": "hallo@kartenherz.com",
          "name": "Kartenherz"
        }
      ],
      "cc": [],
      "subject": "Frage zu Hochzeitskarten",
      "snippet": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung …",
      "received_at": "2026-10-09T14:05:00+02:00",
      "is_seen": false,
      "is_flagged": false,
      "is_answered": false,
      "has_attachments": false,
      "has_calendar": false,
      "category": "lead",
      "category_label": "Leads & Anfragen",
      "priority": "high",
      "sentiment": "positive",
      "intent": "Angebot für 80 Hochzeitskarten",
      "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
      "needs_reply": true,
      "contact_id": 42,
      "campaign_id": null,
      "labels": ["Hochzeit"],
      "snoozed_until": null,
      "follow_up_at": null,
      "replied_at": null
    }
  }
}
inbox.replied Mail aus dem Posteingang gesendet Eine Antwort, Weiterleitung oder neue Mail wurde aus dem Posteingang verschickt – von Dir, nach Deiner Freigabe oder vom KI-Autopiloten.
Felder in data
FeldBeschreibung
draft integer Der KI-Entwurf (inbox.approval_needed, inbox.auto_replied) – bei inbox.replied nur seine ID (= draft_id).
draft_id integer ID der gesendeten Mail (GET /inbox/drafts/{id}).
mailbox_id integer ID des Postfachs.
kind string reply, reply_all, forward, new oder auto.
source string Quelle bzw. Auslöser (bei inbox.replied: user, ai oder rule).
auto boolean true, wenn der KI-Autopilot die Antwort gesendet hat.
to array<string> Empfänger (To und Cc, E-Mail-Adressen).
subject string Betreff.
message_id string ID der versendeten Nachricht.
sent_at string (Datum/Zeit) Zeitpunkt des Versands oder null.
in_reply_to_message integer ID der beantworteten Mail (GET /inbox/messages/{id}) oder null.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "inbox.replied",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "draft": 5501,
    "draft_id": 5501,
    "mailbox_id": 2,
    "kind": "reply",
    "source": "ai",
    "auto": false,
    "to": ["anna@beispiel.de"],
    "subject": "Re: Frage zu Hochzeitskarten",
    "message_id": "<6f1c0d9a2b7e4c1f@kartenherz.com>",
    "sent_at": "2026-10-09T14:21:04+02:00",
    "in_reply_to_message": 9120
  }
}
inbox.approval_needed KI-Entwurf wartet auf Freigabe Die KI hat eine Antwort entworfen, die Du freigeben musst (Modus „Mit Freigabe“ oder der Autopilot hat eine Sicherheitsprüfung nicht bestanden).
Felder in data
FeldBeschreibung
draft object Der KI-Entwurf (inbox.approval_needed, inbox.auto_replied) – bei inbox.replied nur seine ID (= draft_id).
draft.id integer Eindeutige ID.
draft.mailbox_id integer ID des Postfachs.
draft.subject string Betreff der Antwort.
draft.to array<object> Empfänger (Liste aus {email, name}).
draft.to[].email string E-Mail-Adresse.
draft.to[].name string Name (kann leer sein).
draft.ai_confidence integer Selbsteinschätzung der KI (0–100) oder null.
draft.ai_reason string Begründung der KI bzw. warum der Autopilot nicht selbst gesendet hat.
draft.ai_flags array<string> Risiken: legal, money, medical, threat, prompt_injection, personal_data, unclear.
draft.send_at string · null Geplanter Versand (Autopilot: nach der Wartezeit) oder null.
message Mail-Objekt (Posteingang) Testnachricht (test.ping) bzw. die Mail im Posteingang (inbox.*). Mail-Objekt (Posteingang)
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "inbox.approval_needed",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "draft": {
      "id": 5502,
      "mailbox_id": 2,
      "subject": "Re: Rechnung doppelt abgebucht?",
      "to": [
        {
          "email": "ben@beispiel.de",
          "name": "Ben Krüger"
        }
      ],
      "ai_confidence": 64,
      "ai_reason": "Nicht automatisch gesendet:\n- Kategorie „Beschwerde“ beantwortest Du immer selbst.\n- KI-Risiko: Geld/Erstattung.\nKI-Einschätzung: Ben bittet um Erstattung einer doppelten Abbuchung.",
      "ai_flags": ["money"],
      "send_at": null
    },
    "message": {
      "id": 9133,
      "mailbox_id": 2,
      "mailbox_email": "hallo@kartenherz.com",
      "folder": "Posteingang",
      "folder_role": "inbox",
      "thread_key": "8d2e4b6f0a1c3e5d7f9b1a2c4e6d8f0a1b3c5d7e",
      "message_id": "<rechnung-77@beispiel.de>",
      "direction": "in",
      "from": {
        "email": "ben@beispiel.de",
        "name": "Ben Krüger"
      },
      "to": [
        {
          "email": "hallo@kartenherz.com",
          "name": "Kartenherz"
        }
      ],
      "cc": [],
      "subject": "Rechnung doppelt abgebucht?",
      "snippet": "Hallo, mir wurde die Rechnung 2026-1042 zweimal abgebucht. Bitte erstattet mir den Betrag …",
      "received_at": "2026-10-09T14:05:00+02:00",
      "is_seen": false,
      "is_flagged": false,
      "is_answered": false,
      "has_attachments": false,
      "has_calendar": false,
      "category": "complaint",
      "category_label": "Beschwerde",
      "priority": "high",
      "sentiment": "negative",
      "intent": "Erstattung einer doppelten Abbuchung",
      "summary": "Ben wurde die Rechnung doppelt abgebucht und bittet um Erstattung.",
      "needs_reply": true,
      "contact_id": 57,
      "campaign_id": null,
      "labels": [],
      "snoozed_until": null,
      "follow_up_at": null,
      "replied_at": null
    }
  }
}
inbox.auto_replied Autopilot-Antwort geplant Der KI-Autopilot hat eine Antwort automatisch eingeplant. Sie geht nach der eingestellten Verzögerung raus und kann bis dahin gestoppt werden.
Felder in data
FeldBeschreibung
draft object Der KI-Entwurf (inbox.approval_needed, inbox.auto_replied) – bei inbox.replied nur seine ID (= draft_id).
draft.id integer Eindeutige ID.
draft.mailbox_id integer ID des Postfachs.
draft.subject string Betreff der Antwort.
draft.to array<object> Empfänger (Liste aus {email, name}).
draft.to[].email string E-Mail-Adresse.
draft.to[].name string Name (kann leer sein).
draft.ai_confidence integer Selbsteinschätzung der KI (0–100) oder null.
draft.ai_reason string Begründung der KI bzw. warum der Autopilot nicht selbst gesendet hat.
draft.ai_flags array Risiken: legal, money, medical, threat, prompt_injection, personal_data, unclear.
draft.send_at string (Datum/Zeit) Geplanter Versand (Autopilot: nach der Wartezeit) oder null.
message Mail-Objekt (Posteingang) Testnachricht (test.ping) bzw. die Mail im Posteingang (inbox.*). Mail-Objekt (Posteingang)
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "inbox.auto_replied",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "draft": {
      "id": 5503,
      "mailbox_id": 2,
      "subject": "Re: Frage zu Hochzeitskarten",
      "to": [
        {
          "email": "anna@beispiel.de",
          "name": "Anna Schmidt"
        }
      ],
      "ai_confidence": 91,
      "ai_reason": "Alle Sicherheitsprüfungen bestanden – wird automatisch beantwortet.\nKI-Einschätzung: Preis und Lieferzeit stehen im Wissen für die KI.",
      "ai_flags": [],
      "send_at": "2026-10-09T14:20:00+02:00"
    },
    "message": {
      "id": 9120,
      "mailbox_id": 2,
      "mailbox_email": "hallo@kartenherz.com",
      "folder": "Posteingang",
      "folder_role": "inbox",
      "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
      "message_id": "<CAF-anfrage-4711@mail.beispiel.de>",
      "direction": "in",
      "from": {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      },
      "to": [
        {
          "email": "hallo@kartenherz.com",
          "name": "Kartenherz"
        }
      ],
      "cc": [],
      "subject": "Frage zu Hochzeitskarten",
      "snippet": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung …",
      "received_at": "2026-10-09T14:05:00+02:00",
      "is_seen": false,
      "is_flagged": false,
      "is_answered": false,
      "has_attachments": false,
      "has_calendar": false,
      "category": "lead",
      "category_label": "Leads & Anfragen",
      "priority": "high",
      "sentiment": "positive",
      "intent": "Angebot für 80 Hochzeitskarten",
      "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
      "needs_reply": true,
      "contact_id": 42,
      "campaign_id": null,
      "labels": ["Hochzeit"],
      "snoozed_until": null,
      "follow_up_at": null,
      "replied_at": null
    }
  }
}
test.ping Test-Ping Test-Ereignis, das Du auf der Entwickler-Seite oder per API auslöst.
Felder in data
FeldBeschreibung
message string Testnachricht (test.ping) bzw. die Mail im Posteingang (inbox.*).
webhook_id integer ID des getesteten Webhooks.
account object Konto, zu dem der Webhook gehört.
account.id integer ID des Kontos.
account.email string E-Mail-Adresse des Kontos.
Beispiel-Payload JSON
{
  "id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "event": "test.ping",
  "created_at": "2026-10-09T14:05:00+02:00",
  "data": {
    "message": "Hallo von Persolz! Dieser Test zeigt, dass Dein Endpunkt Webhooks empfängt.",
    "webhook_id": 4,
    "account": {
      "id": 2,
      "email": "du@beispiel.de"
    }
  }
}

Webhooks empfangen und verifizieren

Prüfe bei jeder Nachricht die Signatur – nur so weißt Du, dass sie wirklich von Persolz kommt und unterwegs nicht verändert wurde. Der Header X-Persolz-Signature sieht so aus: t=1791560735,v1=5f3a8c…

  1. Rohen Body lesen – genau die Bytes, die angekommen sind. Nicht erst als JSON einlesen und wieder ausgeben, sonst stimmt die Signatur nicht mehr.
  2. Header zerlegen: t ist der Zeitstempel (Unix-Sekunden), v1 die Signatur.
  3. Erwartete Signatur berechnen: HMAC-SHA256 mit Deinem Secret über "<t>.<roher Body>", als Hex-String.
  4. Zeitkonstant vergleichen (hash_equals, timingSafeEqual, compare_digest) und Zeitstempel ablehnen, die älter als 5 Minuten sind (Schutz vor Wiederholungsangriffen).
  5. Schnell antworten (200) und aufwendige Arbeit danach erledigen. Entdopple über X-Persolz-Event-Id – bei Wiederholungen bleibt sie gleich.

Zum Testen schickt der Button „Test senden“ auf der Entwickler-Seite (oder POST /webhooks/{id}/test) ein Ereignis test.ping. Nach einem Secret-Wechsel (rotate_secret) ist das alte Secret sofort ungültig.

Signatur prüfen
<?php
// webhook.php – empfängt Ereignisse von Persolz
$secret = getenv('PERSOLZ_WEBHOOK_SECRET'); // whsec_… aus „API & Doku“ → Webhooks

$body   = file_get_contents('php://input');          // 1. ROHEN Body lesen
$header = $_SERVER['HTTP_X_PERSOLZ_SIGNATURE'] ?? ''; //    t=1791560735,v1=5f3a…

// 2. Header zerlegen
$t = 0;
$signatures = [];
foreach (explode(',', $header) as $part) {
    [$k, $v] = array_pad(explode('=', trim($part), 2), 2, '');
    if ($k === 't') { $t = (int) $v; }
    if ($k === 'v1') { $signatures[] = $v; }
}

// 3. Erwartete Signatur berechnen und zeitkonstant vergleichen
$expected = hash_hmac('sha256', $t . '.' . $body, $secret);
$valid = false;
foreach ($signatures as $sig) {
    $valid = $valid || hash_equals($expected, $sig);
}

// 4. Zu alte Zeitstempel ablehnen (Schutz vor Replay)
if (!$valid || abs(time() - $t) > 300) {
    http_response_code(400);
    exit('Ungültige Signatur');
}

// 5. Schnell bestätigen, dann verarbeiten
$event = json_decode($body, true);
http_response_code(200);
echo 'ok';

// Doppelte Zustellungen erkennen (gleiche Event-ID bei Wiederholungen)
$eventId = $_SERVER['HTTP_X_PERSOLZ_EVENT_ID'] ?? $event['id'];
// if (bereitsVerarbeitet($eventId)) return;

switch ($event['event']) {
    case 'contact.created':
        // $event['data']['contact']['email'] …
        break;
    case 'contact.unsubscribed':
        // Kunde im Shop als „keine Werbung“ markieren
        break;
}
// webhook.js – Node.js 18+ ohne weitere Pakete
const http = require('node:http');
const crypto = require('node:crypto');

const SECRET = process.env.PERSOLZ_WEBHOOK_SECRET; // whsec_…

function verify(rawBody, header) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('=', 2))
  );
  const t = Number(parts.t);
  if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac('sha256', SECRET)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

http.createServer((req, res) => {
  let raw = '';
  req.setEncoding('utf8');
  req.on('data', (chunk) => { raw += chunk; });
  req.on('end', () => {
    // Wichtig: den ROHEN Body prüfen – nicht JSON.stringify(JSON.parse(raw))
    if (!verify(raw, req.headers['x-persolz-signature'] || '')) {
      res.writeHead(400).end('Ungültige Signatur');
      return;
    }
    res.writeHead(200).end('ok'); // sofort bestätigen (max. 4 Sekunden)

    const event = JSON.parse(raw);
    console.log(event.event, req.headers['x-persolz-event-id'], event.data);
  });
}).listen(3000);
# webhook.py – mit Flask (pip install flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["PERSOLZ_WEBHOOK_SECRET"].encode()

@app.post("/hooks/persolz")
def persolz_webhook():
    raw = request.get_data()  # roher Body (bytes)
    parts = dict(p.strip().split("=", 1) for p in
                 request.headers.get("X-Persolz-Signature", "").split(",") if "=" in p)
    t = int(parts.get("t", 0))
    expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - t) > 300:
        abort(400)

    event = request.get_json()
    print(event["event"], request.headers.get("X-Persolz-Event-Id"))
    return "ok", 200
POST Test-Ping auslösen
curl -X POST https://persolz.com/api/v1/webhooks/4/test \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks/4/test');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks/4/test', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/webhooks/4/test",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

Anleitung

Funnel-Integration (z. B. kartenherz.com)

Dein Funnel sammelt Leads – Persolz macht daraus Beziehungen. So landet jede Anmeldung von Deiner Landingpage (hier am Beispiel kartenherz.com) in der richtigen Liste und startet automatisch Deine Seifenoper.

Welcher Weg passt zu Dir?

A) Server-seitigB) HTML-FormularC) JavaScript + Schlüssel
Ideal füreigener Formular-Handler, Shop, WordPress-PluginLandingpage-Baukästen, statische Seitengeschützte Bereiche, interne Tools
API-Schlüsselja – bleibt geheim auf dem Servernein (öffentlicher Listen-Token)ja – im Browser sichtbar
Liste & Tags✓✓✓
Seifenoper, eigene Felder✓ direktüber die Listen-Automation✓ direkt
Sicherheitsehr hochhoch (Spamschutz eingebaut)nur mit Einschränkungen

Vorbereitung: Lege in Persolz eine Liste an (z. B. „Kartenherz Funnel“) und eine Seifenoper (z. B. „Willkommen bei Kartenherz“). Die IDs und den öffentlichen Token (public_token) der Liste liefert GET /lists. Für B stellst Du in der Seifenoper den Auslöser „Beim Eintrag in Liste“ ein – dann startet sie automatisch.

GET IDs und Token herausfinden
curl https://persolz.com/api/v1/lists \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/lists",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

A) Server-seitig mit PHP (empfohlen)

Das Formular auf kartenherz.com schickt die Daten an Dein eigenes Skript (anmelden.php). Erst Dein Server spricht mit Persolz – der Schlüssel verlässt ihn nie.

Das Skript prüft die Eingaben, ruft POST /contacts auf und übergibt in einem Rutsch: Liste (list_ids), Tags, eigene Felder und die Seifenoper (soap_sequence_id). Der Kontakt wird angelegt oder – falls er schon existiert – aktualisiert.

  • Double-Opt-in: Ist die Liste darauf eingestellt, bekommt der Kontakt eine Bestätigungsmail und bleibt pending. Eine Seifenoper kann erst nach der Bestätigung starten – nutze dafür den Auslöser „Beim Eintrag in Liste“ oder die Willkommens-Seifenoper der Liste. result.soap_enrollment sagt Dir, was passiert ist.
  • Einwilligung: consent: true bestätigt eine neue Einwilligung – nötig, um früher abgemeldete Adressen wieder anzumelden.
  • Doppelte Anfragen: Der Idempotency-Key wird aus dem Inhalt der Anmeldung gebildet – schickt jemand das Formular doppelt ab, passiert trotzdem alles nur einmal.
anmelden.php PHP
<?php
// anmelden.php – empfängt das Funnel-Formular von kartenherz.com (server-seitig)
const PERSOLZ_API = 'https://persolz.com/api/v1';
const LIST_ID     = 3;   // Liste „Kartenherz Funnel“
const SEQUENCE_ID = 7;   // Seifenoper „Willkommen bei Kartenherz“

$apiKey = getenv('PERSOLZ_API_KEY'); // Schlüssel nur auf dem Server, nie im HTML

// 1. Eingaben prüfen (Honeypot „website“ muss leer sein)
$email = trim($_POST['email'] ?? '');
if (!empty($_POST['website']) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
    header('Location: /anmeldung?fehler=email', true, 303);
    exit;
}

// 2. Kontakt anlegen oder aktualisieren – inkl. Liste, Tags und Seifenoper
$payload = [
    'email'            => $email,
    'first_name'       => trim($_POST['vorname'] ?? ''),
    'list_ids'         => [LIST_ID],
    'tags'             => ['funnel', 'kartenherz'],
    'custom_fields'    => ['quelle' => $_POST['utm_source'] ?? 'direkt'],
    'soap_sequence_id' => SEQUENCE_ID,
    'consent'          => isset($_POST['einwilligung']),
];

$requestBody = json_encode($payload);

$ch = curl_init(PERSOLZ_API . '/contacts');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        // gleiche Anmeldung = gleicher Schlüssel → doppeltes Absenden legt nichts doppelt an
        'Idempotency-Key: funnel-' . hash('sha256', $requestBody),
    ],
    CURLOPT_POSTFIELDS     => $requestBody,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 15,
]);
$body   = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = json_decode($body, true);

// 3. Ergebnis auswerten
if ($status === 200 || $status === 201) {
    $result = $json['data']['result'];
    // pending = Double-Opt-in: die Seifenoper startet nach der Bestätigung
    $ziel = $result['status'] === 'pending' ? '/bitte-bestaetigen' : '/danke';
    header('Location: ' . $ziel, true, 303);
    exit;
}

error_log('Persolz-Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body)
    . ' (Request-ID ' . ($json['error']['request_id'] ?? '-') . ')');
header('Location: /anmeldung?fehler=technik', true, 303);
Formular auf der Landingpage HTML
<!-- Formular auf kartenherz.com – sendet an Dein eigenes anmelden.php -->
<form action="/anmelden.php" method="post">
  <label>Vorname <input name="vorname" autocomplete="given-name"></label>
  <label>E-Mail <input name="email" type="email" required autocomplete="email"></label>
  <!-- Honeypot gegen Bots: per CSS ausblenden, muss leer bleiben -->
  <input name="website" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px">
  <label><input type="checkbox" name="einwilligung" value="1" required>
    Ja, ich möchte die Kartenherz-Mails erhalten (Abmeldung jederzeit möglich).</label>
  <button type="submit">Gratis-Kartenset sichern</button>
</form>

B) Reines HTML-Formular – ohne Server-Code

Jede Liste hat eine öffentliche Anmelde-URL: POST /subscribe/{token}. Sie braucht keinen API-Schlüssel und darf von jeder Website aufgerufen werden. Spamschutz ist eingebaut: Honeypot-Feld website, max. 10 Anmeldungen pro Minute je IP und 200 pro Tag je Liste.

  • Weiterleitung: Mit success_url landet der Besucher nach der Anmeldung auf Deiner Danke-Seite (303, mit ?status=subscribed bzw. ?status=pending bei Double-Opt-in). Bei Fehlern geht es zu error_url mit ?error=…&code=… – ohne error_url zeigt Persolz eine kleine Fehlerseite. Trägst Du bei der Liste Erlaubte Weiterleitungs-Domains ein (Web-Oberfläche oder redirect_domains), werden Ziele auf anderen Hosts ignoriert; error_url gilt nur bei gültigem Listen-Token.
  • Tags als kommagetrennter Text (max. 10). Die Seifenoper startet über den Listen-Auslöser bzw. nach der Double-Opt-in-Bestätigung.
  • Noch einfacher: Bette das fertige Formular per <iframe> ein (GET /subscribe/{token}/form) – Felder, Button-Text, Farben und Weiterleitung stellst Du per URL ein.
  • Mit JavaScript: Sendest Du JSON (Accept: application/json), bekommst Du statt der Weiterleitung eine JSON-Antwort (201/200, Fehler 422/429) – ideal für Formulare ohne Seitenwechsel.
HTML-Formular HTML
<!-- Reines HTML – kein Server-Code, kein API-Schlüssel nötig -->
<form action="https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c"
      method="post" accept-charset="UTF-8">
  <input type="hidden" name="success_url" value="https://kartenherz.com/danke">
  <input type="hidden" name="error_url" value="https://kartenherz.com/anmeldung">
  <input type="hidden" name="tags" value="funnel,kartenherz">

  <label>Vorname <input name="first_name" autocomplete="given-name"></label>
  <label>E-Mail <input name="email" type="email" required autocomplete="email"></label>

  <!-- Honeypot: muss leer bleiben, Bots füllen es aus -->
  <div style="position:absolute;left:-9999px" aria-hidden="true">
    <input name="website" tabindex="-1" autocomplete="off">
  </div>

  <label><input type="checkbox" name="consent" value="1" required>
    Ja, ich möchte E-Mails von Kartenherz erhalten.</label>
  <button type="submit">Jetzt anmelden</button>
</form>
Als iframe einbetten HTML
<!-- Alternative: fertiges Formular von Persolz per iframe einbetten -->
<iframe src="https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form?fields=first_name&button=Gratis-Kartenset%20sichern&success_url=https%3A%2F%2Fkartenherz.com%2Fdanke"
        title="Newsletter-Anmeldung" width="100%" height="380"
        style="border:0;max-width:480px" loading="lazy"></iframe>
Per fetch absenden (ohne Schlüssel) JavaScript
// Formular per JavaScript absenden – ohne API-Schlüssel (CORS ist für alle Seiten offen)
const form = document.querySelector('#anmeldung');
form.addEventListener('submit', async (event) => {
  event.preventDefault();
  const response = await fetch('https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
    body: JSON.stringify({
      email: form.email.value,
      first_name: form.vorname.value,
      tags: ['funnel', 'kartenherz'],
      website: form.website.value // Honeypot
    })
  });
  const json = await response.json();
  if (response.ok) {
    // json.data.state: "subscribed" oder "pending" (Bestätigungsmail unterwegs)
    form.innerHTML = '<p>' + json.data.message + '</p>';
  } else {
    alert(json.error.message); // z. B. „Bitte prüfe Deine E-Mail-Adresse …“
  }
});

C) JavaScript mit Origin-beschränktem Schlüssel

Manchmal soll der Browser mehr können als nur anmelden – etwa in einem Mitgliederbereich, in dem eingeloggte Kunden eine Story-Serie starten. Dann kann der Browser die API direkt aufrufen, aber nur mit einem eigens dafür erstellten, eingeschränkten Schlüssel:

  1. Scope: nur write (kein send, kein read, kein ai).
  2. Erlaubte Origins: https://kartenherz.com (ggf. https://*.kartenherz.com).
  3. Nur für diesen Zweck: ein eigener Schlüssel, den Du nirgends sonst verwendest – so kannst Du ihn jederzeit gezielt sperren.
  4. Regelmäßig erneuern und sofort widerrufen, wenn er auffällig wird (API-Protokoll prüfen).

Achtung: Auch ein eingeschränkter Schlüssel ist im Quelltext sichtbar und könnte außerhalb des Browsers missbraucht werden – mit write ließen sich z. B. Kontakte ändern oder löschen. Für öffentliche Seiten nimm deshalb immer Weg B oder A.

Kontakt aus dem Browser eintragen JavaScript
// Nur mit einem eingeschränkten Schlüssel: Scope „Schreiben“, erlaubte Origin
// https://kartenherz.com, nur für diesen Zweck. Der Schlüssel ist im Browser sichtbar!
const PERSOLZ_KEY = 'pz_live_DEIN_API_SCHLUESSEL';

async function persolzAnmelden(email, vorname) {
  const response = await fetch('https://persolz.com/api/v1/contacts', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + PERSOLZ_KEY,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID()
    },
    body: JSON.stringify({
      email,
      first_name: vorname,
      list_ids: [3],
      tags: ['mitgliederbereich'],
      soap_sequence_id: 7,
      consent: true
    })
  });
  const json = await response.json();
  if (!response.ok) {
    // 403 forbidden mit reason "origin_not_allowed" = Seite nicht freigegeben
    throw new Error(json.error.message);
  }
  return json.data; // Kontakt inkl. result.soap_enrollment
}

Seifenoper gezielt starten

Es gibt drei Wege, einen Kontakt in eine Seifenoper zu bringen:

  • Beim Anlegen: soap_sequence_id in POST /contacts (nur für aktive, also nicht pending-Kontakte).
  • Nachträglich: POST /sequences/{id}/enroll mit contact_ids, emails oder einer ganzen Liste (list_id) – optional mit start_date (tomorrow, 2026-10-12 …). Unbekannte Adressen werden dabei als Kontakte angelegt.
  • Automatisch: Auslöser „Beim Eintrag in Liste“ der Seifenoper oder die Willkommens-Seifenoper der Liste (welcome_soap_id). Das funktioniert mit allen drei Wegen und auch mit Double-Opt-in.

Mit dem Webhook soap.enrolled erfährt Dein System, wann jemand startet, mit soap.completed, wann er alle Tage erhalten hat.

POST Kontakte aufnehmen
curl -X POST https://persolz.com/api/v1/sequences/7/enroll \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "emails": ["anna@beispiel.de"],
    "start_date": "tomorrow"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/7/enroll');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'emails' => ['anna@beispiel.de'],
        'start_date' => 'tomorrow',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/7/enroll', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    emails: ['anna@beispiel.de'],
    start_date: 'tomorrow'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/sequences/7/enroll",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "emails": ["anna@beispiel.de"],
        "start_date": "tomorrow",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

Anleitung

Transaktionale E-Mails per API mit Platzhaltern

Bestellbestätigung, Terminbestätigung, Follow-up nach dem Kauf: Mit POST /emails/send verschickst Du einzelne E-Mails direkt aus Deinem System – über Dein eigenes SMTP-Konto, mit Platzhaltern, Vorlagen und Anhängen.

Eine Bestellbestätigung senden

  • to: eine E-Mail-Adresse, mehrere, oder Kontakt-IDs. Unbekannte Adressen bekommen die Mail trotzdem; ist die Adresse ein Kontakt, werden seine Daten ({{vorname}}, eigene Felder …) eingesetzt.
  • placeholders: Werte nur für diese Anfrage, z. B. Bestellnummer und Betrag. Für jeden Kunden eine eigene Anfrage.
  • Idempotency-Key: Nimm etwas Eindeutiges aus Deinem Shop, z. B. shop-bestellung-4711 – dann geht die Mail garantiert nur einmal raus, egal wie oft Dein Shop es versucht.
  • track_opens / track_clicks: Für rein informative Mails kannst Du das Tracking abschalten.
  • scheduled_at: Für Follow-ups in der Zukunft, z. B. „Wie gefällt Dir Dein Kartenset?“ drei Tage später.

Die API antwortet mit 202 Accepted und einer campaign_id. Wichtig: Abgemeldete und unbestätigte (pending) Adressen werden immer übersprungen – auch bei Transaktionsmails.

POST Bestellbestätigung
curl -X POST https://persolz.com/api/v1/emails/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: shop-bestellung-4711" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "anna@beispiel.de",
    "subject": "Danke für Deine Bestellung #{{bestellnummer}}, {{vorname|Du}}!",
    "html": "<p>Hallo {{vorname|Du}},</p><p>Deine Bestellung <strong>#{{bestellnummer}}</strong> über {{betrag}} ist eingegangen.</p><p>Lieferung voraussichtlich am {{lieferdatum}}.</p>",
    "placeholders": {
      "bestellnummer": "4711",
      "betrag": "24,90 €",
      "lieferdatum": "14.10.2026"
    },
    "from_name": "Kartenherz",
    "reply_to": "hallo@kartenherz.com",
    "track_opens": false
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/emails/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: shop-bestellung-4711',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => 'anna@beispiel.de',
        'subject' => 'Danke für Deine Bestellung #{{bestellnummer}}, {{vorname|Du}}!',
        'html' => '<p>Hallo {{vorname|Du}},</p><p>Deine Bestellung <strong>#{{bestellnummer}}</strong> über {{betrag}} ist eingegangen.</p><p>Lieferung voraussichtlich am {{lieferdatum}}.</p>',
        'placeholders' => [
            'bestellnummer' => '4711',
            'betrag' => '24,90 €',
            'lieferdatum' => '14.10.2026',
        ],
        'from_name' => 'Kartenherz',
        'reply_to' => 'hallo@kartenherz.com',
        'track_opens' => false,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/emails/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'shop-bestellung-4711'
  },
  body: JSON.stringify({
    to: 'anna@beispiel.de',
    subject: 'Danke für Deine Bestellung #{{bestellnummer}}, {{vorname|Du}}!',
    html: '<p>Hallo {{vorname|Du}},</p><p>Deine Bestellung <strong>#{{bestellnummer}}</strong> über {{betrag}} ist eingegangen.</p><p>Lieferung voraussichtlich am {{lieferdatum}}.</p>',
    placeholders: {
      bestellnummer: '4711',
      betrag: '24,90 €',
      lieferdatum: '14.10.2026'
    },
    from_name: 'Kartenherz',
    reply_to: 'hallo@kartenherz.com',
    track_opens: false
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/emails/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": "shop-bestellung-4711",
    },
    json={
        "to": "anna@beispiel.de",
        "subject": "Danke für Deine Bestellung #{{bestellnummer}}, {{vorname|Du}}!",
        "html": "<p>Hallo {{vorname|Du}},</p><p>Deine Bestellung <strong>#{{bestellnummer}}</strong> über {{betrag}} ist eingegangen.</p><p>Lieferung voraussichtlich am {{lieferdatum}}.</p>",
        "placeholders": {
            "bestellnummer": "4711",
            "betrag": "24,90 €",
            "lieferdatum": "14.10.2026",
        },
        "from_name": "Kartenherz",
        "reply_to": "hallo@kartenherz.com",
        "track_opens": False,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort (202) JSON
{
  "data": {
    "campaign_id": 813,
    "status": "sending",
    "scheduled_at": null,
    "recipients": 1,
    "queued": 1,
    "sent": 1,
    "failed": 0,
    "message": "Versand abgeschlossen: 1 E-Mail verschickt."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Mit Vorlage und Anhang

Statt eigenem HTML kannst Du eine Vorlage verwenden: template_key für die Bibliothek (Übersicht: GET /templates?category=email) oder template_id für eigene Vorlagen. Betreff und Vorschautext kommen aus der Vorlage, wenn Du keine angibst.

Anhänge lädst Du einmal hoch (POST /attachments, max. 10 MB) und gibst danach nur noch die ID in attachment_ids mit – z. B. Rechnung, AGB oder ein Gutschein als PDF.

Bilder (JPG, PNG, GIF, WebP) aus der Anhang-Cloud baust Du direkt ins HTML ein: POST /attachments und GET /attachments/{id} liefern dafür image_url (die Liste GET /attachments zeigt nur bereits vorhandene Links) – eine öffentliche, nicht erratbare Adresse für <img src="…">. So wird das Bild nicht angehängt, sondern beim Öffnen der Mail geladen.

POST Anhang hochladen
curl -X POST https://persolz.com/api/v1/attachments \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -F "file=@Rechnung-4711.pdf" \
  -F "description=Rechnung zur Bestellung 4711"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/attachments');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_POSTFIELDS     => [
        'file' => new CURLFile('Rechnung-4711.pdf'),
        'description' => 'Rechnung zur Bestellung 4711',
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const form = new FormData();
form.append('file', document.querySelector('input[type=file]').files[0]); // Rechnung-4711.pdf
form.append('description', 'Rechnung zur Bestellung 4711');

const response = await fetch('https://persolz.com/api/v1/attachments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  },
  body: form
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/attachments",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    files={"file": open("Rechnung-4711.pdf", "rb")},
    data={
        "description": "Rechnung zur Bestellung 4711",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
POST Mail mit Vorlage und Anhang
curl -X POST https://persolz.com/api/v1/emails/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: shop-rechnung-4711" \
  -H "Content-Type: application/json" \
  -d '{
    "to": [42],
    "template_key": "email-07",
    "subject": "Deine Rechnung #{{bestellnummer}}",
    "placeholders": {
      "bestellnummer": "4711"
    },
    "attachment_ids": [9]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/emails/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: shop-rechnung-4711',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => [42],
        'template_key' => 'email-07',
        'subject' => 'Deine Rechnung #{{bestellnummer}}',
        'placeholders' => [
            'bestellnummer' => '4711',
        ],
        'attachment_ids' => [9],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/emails/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'shop-rechnung-4711'
  },
  body: JSON.stringify({
    to: [42],
    template_key: 'email-07',
    subject: 'Deine Rechnung #{{bestellnummer}}',
    placeholders: {
      bestellnummer: '4711'
    },
    attachment_ids: [9]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/emails/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": "shop-rechnung-4711",
    },
    json={
        "to": [42],
        "template_key": "email-07",
        "subject": "Deine Rechnung #{{bestellnummer}}",
        "placeholders": {
            "bestellnummer": "4711",
        },
        "attachment_ids": [9],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

Status verfolgen

Mit der campaign_id fragst Du jederzeit den Stand ab: GET /emails/{id} liefert Status und Kennzahlen (versendet, geöffnet, geklickt, fehlgeschlagen). Oder Du lässt Dich per Webhook benachrichtigen: campaign.sent, sobald alles raus ist, sowie email.opened und email.clicked.

GET Status abfragen
curl https://persolz.com/api/v1/emails/813 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/emails/813');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/emails/813', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/emails/813",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

Anleitung

Seinfeld-Mails per API planen

Seinfeld-Mails sind kurze, unterhaltsame Story-Mails im festen Rhythmus – über scheinbar „nichts“, mit einer Brücke zu Deinem Angebot. Per API füllst Du die Themenliste, schreibst oder generierst Mails und planst sie in Deinen Rhythmus ein.

1. Rhythmus und Empfänger festlegen

Mit PUT /seinfeld/settings stellst Du ein, wann (z. B. werktags um 9:00 Uhr) und an wen (z. B. Liste 3) Deine Seinfeld-Mails gehen. Nur die übergebenen Felder ändern sich. Mit auto_generate schreibt die KI Entwürfe für freie Termine, mit auto_send sogar ohne Deine Freigabe.

PUT Einstellungen speichern
curl -X PUT https://persolz.com/api/v1/seinfeld/settings \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "frequency": "weekdays",
    "send_time": "09:00",
    "audience": {
      "lists": [3]
    },
    "offer_name": "Kartenset-Abo",
    "offer_url": "https://kartenherz.com/abo"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/settings');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'enabled' => true,
        'frequency' => 'weekdays',
        'send_time' => '09:00',
        'audience' => [
            'lists' => [3],
        ],
        'offer_name' => 'Kartenset-Abo',
        'offer_url' => 'https://kartenherz.com/abo',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/settings', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    enabled: true,
    frequency: 'weekdays',
    send_time: '09:00',
    audience: {
      lists: [3]
    },
    offer_name: 'Kartenset-Abo',
    offer_url: 'https://kartenherz.com/abo'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.put(
    "https://persolz.com/api/v1/seinfeld/settings",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "enabled": True,
        "frequency": "weekdays",
        "send_time": "09:00",
        "audience": {
            "lists": [3],
        },
        "offer_name": "Kartenset-Abo",
        "offer_url": "https://kartenherz.com/abo",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

2. Themen sammeln

Die Themenliste ist Dein Ideen-Speicher. Mit topics legst Du mehrere auf einmal an; doppelte werden übersprungen. Themen mit Stern (priority) kommen zuerst dran.

POST Themen anlegen
curl -X POST https://persolz.com/api/v1/seinfeld/topics \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "topics": [
      "Der verlorene Schlüssel",
      "Warum ich Briefmarken sammle",
      "Der Kaffeebecher im Büro"
    ]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/topics');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'topics' => [
            'Der verlorene Schlüssel',
            'Warum ich Briefmarken sammle',
            'Der Kaffeebecher im Büro',
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/topics', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    topics: [
      'Der verlorene Schlüssel',
      'Warum ich Briefmarken sammle',
      'Der Kaffeebecher im Büro'
    ]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/topics",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "topics": [
            "Der verlorene Schlüssel",
            "Warum ich Briefmarken sammle",
            "Der Kaffeebecher im Büro",
        ],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

3. Freie Termine finden

GET /seinfeld/slots zeigt alle Versandtermine im Zeitraum. Termine mit is_gap: true haben noch keine Mail – dort ist Platz für die nächste Geschichte.

GET Termine abfragen
curl "https://persolz.com/api/v1/seinfeld/slots?from=2026-10-12&to=2026-10-23" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'from' => '2026-10-12',
    'to' => '2026-10-23',
]);
$ch = curl_init('https://persolz.com/api/v1/seinfeld/slots?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  from: '2026-10-12',
  to: '2026-10-23'
});
const response = await fetch('https://persolz.com/api/v1/seinfeld/slots?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/seinfeld/slots",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "from": "2026-10-12",
        "to": "2026-10-23",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

4. Mail schreiben – selbst oder mit KI

  • Selbst geschrieben: POST /seinfeld/emails legt einen Entwurf an. Mit topic_id wird das Thema als verwendet markiert, story_title und story_summary füttern das Story-Gedächtnis (gegen Wiederholungen).
  • Von der KI: POST /seinfeld/generate schreibt eine Mail aus dem nächsten Thema (source: "next_topic"), einem bestimmten Thema oder Deiner Idee. Mit persist: true wird sie direkt als Entwurf gespeichert. Das verbraucht KI-Guthaben (Scope ai).
POST Eigener Entwurf
curl -X POST https://persolz.com/api/v1/seinfeld/emails \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "html": "<p>Heute Morgen ist mir mein Kaffeebecher …</p>",
    "topic_id": 15,
    "story_title": "Der Kaffeebecher"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'subject' => 'Was mein Kaffeebecher über Marketing weiß',
        'html' => '<p>Heute Morgen ist mir mein Kaffeebecher …</p>',
        'topic_id' => 15,
        'story_title' => 'Der Kaffeebecher',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    subject: 'Was mein Kaffeebecher über Marketing weiß',
    html: '<p>Heute Morgen ist mir mein Kaffeebecher …</p>',
    topic_id: 15,
    story_title: 'Der Kaffeebecher'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/emails",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "subject": "Was mein Kaffeebecher über Marketing weiß",
        "html": "<p>Heute Morgen ist mir mein Kaffeebecher …</p>",
        "topic_id": 15,
        "story_title": "Der Kaffeebecher",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
POST Mit KI generieren
curl -X POST https://persolz.com/api/v1/seinfeld/generate \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "next_topic",
    "model": "sonnet",
    "persist": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/generate');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'source' => 'next_topic',
        'model' => 'sonnet',
        'persist' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/generate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    source: 'next_topic',
    model: 'sonnet',
    persist: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/generate",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "source": "next_topic",
        "model": "sonnet",
        "persist": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

5. Freigeben und planen

POST /seinfeld/emails/{id}/schedule gibt die Mail frei und plant sie – zum angegebenen scheduled_at oder, ohne Angabe, zum nächsten freien Termin im Rhythmus. Persolz versendet pünktlich; danach meldet der Webhook campaign.sent (Typ seinfeld, ref_id = ID der Seinfeld-Mail).

Geplant, aber doch nicht? POST /seinfeld/emails/{id}/unschedule macht sie wieder zum Entwurf. Alle geplanten Mails liefert GET /seinfeld/emails?status=planned.

POST Mail planen
curl -X POST https://persolz.com/api/v1/seinfeld/emails/120/schedule \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduled_at": "2026-10-12T09:00:00+02:00"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails/120/schedule');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'scheduled_at' => '2026-10-12T09:00:00+02:00',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails/120/schedule', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    scheduled_at: '2026-10-12T09:00:00+02:00'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/emails/120/schedule",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "scheduled_at": "2026-10-12T09:00:00+02:00",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort (gekürzt) JSON
{
  "data": {
    "id": 120,
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "status": "scheduled",
    "status_label": "Geplant",
    "scheduled_at": "2026-10-12T09:00:00+02:00",
    "campaign_id": 901
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Anleitung

Posteingang per API

Der Posteingang verbindet Deine IMAP-Postfächer mit Persolz: Die KI sortiert neue Mails, schreibt Antwortentwürfe und beantwortet auf Wunsch sichere Anfragen selbst. Per API holst Du Mails in Dein CRM, antwortest aus Deinem System und gibst KI-Entwürfe frei. Das Postfach selbst verbindest Du in der App (Passwörter gibt die API nie heraus).

1. Neue Mails abholen

Persolz ruft Deine Postfächer regelmäßig ab. Brauchst Du es sofort, stößt POST /mailboxes/{id}/sync den Abruf an (max. 8 pro Minute). Danach liefert GET /inbox/messages die Mails aus dem Zwischenspeicher – mit KI-Kategorie, Priorität, Zusammenfassung und Kontaktzuordnung, aber ohne Inhalt.

Zum Abgleichen merkst Du Dir den Zeitpunkt des letzten Abrufs und fragst mit since nur Neues ab. Noch besser: Der Webhook inbox.received meldet jede neue Mail sofort (ohne Newsletter, Abwesenheitsnotizen, Unzustellbarkeitsmeldungen und Spam-Verdacht).

POST Jetzt abrufen
curl -X POST https://persolz.com/api/v1/mailboxes/2/sync \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/mailboxes/2/sync');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/mailboxes/2/sync', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/mailboxes/2/sync",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
GET Neue Leads seit gestern
curl "https://persolz.com/api/v1/inbox/messages?category=lead&since=2026-10-09T00%3A00%3A00%2B02%3A00" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'category' => 'lead',
    'since' => '2026-10-09T00:00:00+02:00',
]);
$ch = curl_init('https://persolz.com/api/v1/inbox/messages?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  category: 'lead',
  since: '2026-10-09T00:00:00+02:00'
});
const response = await fetch('https://persolz.com/api/v1/inbox/messages?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/inbox/messages",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "category": "lead",
        "since": "2026-10-09T00:00:00+02:00",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

2. Lesen und bearbeiten

GET /inbox/messages/{id}?include_body=true liefert Text und bereinigtes HTML (ohne Skripte, externe Bilder blockiert), dazu Anhänge, Verlauf und Entwürfe. Der Abruf markiert die Mail nicht als gelesen.

POST /inbox/messages/actions bearbeitet bis zu 500 Mails auf einmal – genau wie in der App und auch auf dem Mailserver: read, flag, archive, trash, move, snooze (params.until), follow_up (params.at: „Erinnern, falls keine Antwort kommt“), label, set_category …

POST Gelesen und archiviert
curl -X POST https://persolz.com/api/v1/inbox/messages/actions \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [9120, 9121],
    "action": "archive"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/messages/actions');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'ids' => [9120, 9121],
        'action' => 'archive',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/messages/actions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ids: [9120, 9121],
    action: 'archive'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/inbox/messages/actions",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "ids": [9120, 9121],
        "action": "archive",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
POST Bis Montag zurückstellen
curl -X POST https://persolz.com/api/v1/inbox/messages/actions \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [9120],
    "action": "snooze",
    "params": {
      "until": "2026-10-12T09:00:00+02:00"
    }
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/messages/actions');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'ids' => [9120],
        'action' => 'snooze',
        'params' => [
            'until' => '2026-10-12T09:00:00+02:00',
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/messages/actions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ids: [9120],
    action: 'snooze',
    params: {
      until: '2026-10-12T09:00:00+02:00'
    }
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/inbox/messages/actions",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "ids": [9120],
        "action": "snooze",
        "params": {
            "until": "2026-10-12T09:00:00+02:00",
        },
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

3. Antworten

POST /inbox/messages/{id}/reply antwortet über das Postfach der Mail – mit Signatur, zitiertem Original und korrektem Verlauf (In-Reply-To). Empfänger und Betreff werden vorbelegt. Ohne send_at geht die Mail sofort raus (Scope send), die Kopie landet im Ordner „Gesendet“.

Neue Mails und Weiterleitungen schreibst Du mit POST /inbox/drafts (kind: new, reply, reply_all, forward) – sofort, geplant (send_at) oder als Entwurf (send: false). Geplante Mails stoppst Du mit POST /inbox/drafts/{id}/cancel.

POST Antwort senden
curl -X POST https://persolz.com/api/v1/inbox/messages/9120/reply \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: crm-antwort-9120" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p>"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/messages/9120/reply');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: crm-antwort-9120',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'html' => '<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p>',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/messages/9120/reply', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': 'crm-antwort-9120'
  },
  body: JSON.stringify({
    html: '<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p>'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/inbox/messages/9120/reply",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": "crm-antwort-9120",
    },
    json={
        "html": "<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p>",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

4. KI-Entwürfe freigeben

POST /inbox/messages/{id}/ai-draft lässt die KI eine Antwort im Stil Deines Postfachs schreiben – mit Selbsteinschätzung (confidence), Risiken und Begründung (Scope ai, verbraucht KI-Guthaben). Mit save: "approval" landet sie in den Freigaben.

Im Modus „Mit Freigabe“ oder wenn der Autopilot eine Sicherheitsprüfung nicht besteht, meldet der Webhook inbox.approval_needed den Entwurf. GET /inbox/approvals zeigt alle wartenden Entwürfe mit den Gründen; POST /inbox/approvals/{id}/approve sendet (optional mit Änderungen), …/reject lehnt ab – beides fließt als Erkenntnis in künftige Entwürfe ein.

POST Entwurf in die Freigaben
curl -X POST https://persolz.com/api/v1/inbox/messages/9120/ai-draft \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "tone": "herzlich",
    "save": "approval"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/messages/9120/ai-draft');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'tone' => 'herzlich',
        'save' => 'approval',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/messages/9120/ai-draft', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    tone: 'herzlich',
    save: 'approval'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/messages/9120/ai-draft",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "tone": "herzlich",
        "save": "approval",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
POST Freigeben und senden
curl -X POST https://persolz.com/api/v1/inbox/approvals/5502/approve \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/approvals/5502/approve');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/approvals/5502/approve', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Idempotency-Key': crypto.randomUUID()
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/approvals/5502/approve",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])

5. Webhooks des Posteingangs

EreignisWannTypische Verwendung
inbox.receivedneue Mail im PosteingangLead ins CRM, Ticket anlegen
inbox.approval_neededKI-Entwurf wartet auf FreigabeHinweis in Slack/Teams
inbox.auto_repliedAutopilot hat eine Antwort eingeplantProtokoll, Stichproben
inbox.repliedAntwort/Mail aus dem Posteingang gesendetStatus im CRM setzen

Die Payloads enthalten das Mail-Objekt – nie den Inhalt der Mail. Den holst Du bei Bedarf mit GET /inbox/messages/{id}?include_body=true.

Anleitung

Fehlerbehandlung & Wiederholungen

Robuste Integrationen unterscheiden zwischen Fehlern, die sich von selbst erledigen (kurz warten, nochmal), und Fehlern, die Du beheben musst (Eingaben, Rechte, Tarif).

Wann wiederholen – und wann nicht?

SituationWiederholen?So geht's
Netzwerkfehler, Timeoutjamit Pause: 1, 2, 4, 8 … Sekunden (max. 5 Versuche)
429 rate_limitedjagenau so lange warten, wie Retry-After sagt
500 server_error, 503 service_unavailablejamit wachsender Pause; bleibt es dabei, Request-ID an den Support
409 mit reason: "idempotency_in_progress"janach 2 Sekunden mit demselben Key
400, 401, 402, 403, 404, 405, 409, 413, 422neinAnfrage, Schlüssel, Rechte oder Tarif prüfen

Das Wichtigste: Verwende bei POST-Anfragen für alle Versuche denselben Idempotency-Key. Dann ist es egal, ob die erste Anfrage schon angekommen war – es passiert trotzdem nur einmal. Protokolliere bei Fehlern immer die request_id.

Anfrage mit Wiederholung
<?php
/**
 * Persolz-API mit automatischer Wiederholung bei 429, 5xx und Netzwerkfehlern.
 * POST-Anfragen bekommen EINEN Idempotency-Key für alle Versuche.
 */
function persolz(string $method, string $path, ?array $body = null): array
{
    $apiKey = getenv('PERSOLZ_API_KEY');
    $idempotencyKey = bin2hex(random_bytes(16));

    for ($attempt = 1; $attempt <= 5; $attempt++) {
        $headers = ['Authorization: Bearer ' . $apiKey, 'Accept: application/json'];
        if ($body !== null) {
            $headers[] = 'Content-Type: application/json';
        }
        if ($method === 'POST') {
            $headers[] = 'Idempotency-Key: ' . $idempotencyKey;
        }
        $ch = curl_init('https://persolz.com/api/v1' . $path);
        $responseHeaders = [];
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_POSTFIELDS     => $body !== null ? json_encode($body) : null,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$responseHeaders) {
                $p = explode(':', $line, 2);
                if (count($p) === 2) {
                    $responseHeaders[strtolower(trim($p[0]))] = trim($p[1]);
                }
                return strlen($line);
            },
        ]);
        $raw = curl_exec($ch);
        $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
        curl_close($ch);

        $retryable = $raw === false || $status === 429 || $status >= 500;
        if (!$retryable) {
            $json = $status === 204 ? [] : (json_decode((string) $raw, true) ?? []);
            if ($status >= 400) {
                // 4xx: Eingaben oder Rechte prüfen – nicht wiederholen
                throw new RuntimeException(($json['error']['message'] ?? 'API-Fehler') . ' [' . $status . ', '
                    . ($json['error']['request_id'] ?? '-') . ']');
            }
            return $json;
        }
        // Wartezeit: Retry-After (bei 429) oder 1, 2, 4, 8 Sekunden
        $wait = (int) ($responseHeaders['retry-after'] ?? 0) ?: 2 ** ($attempt - 1);
        sleep(min($wait, 30));
    }
    throw new RuntimeException('Persolz ist gerade nicht erreichbar – bitte später erneut versuchen.');
}

// Beispiel
$kontakt = persolz('POST', '/contacts', ['email' => 'anna@beispiel.de', 'list_ids' => [3]]);
// Persolz-API mit Wiederholung bei 429, 5xx und Netzwerkfehlern (Node.js 18+)
async function persolz(method, path, body) {
  const idempotencyKey = crypto.randomUUID(); // gleicher Schlüssel für alle Versuche
  for (let attempt = 1; attempt <= 5; attempt++) {
    let response;
    try {
      response = await fetch('https://persolz.com/api/v1' + path, {
        method,
        headers: {
          'Authorization': `Bearer ${process.env.PERSOLZ_API_KEY}`,
          ...(body ? { 'Content-Type': 'application/json' } : {}),
          ...(method === 'POST' ? { 'Idempotency-Key': idempotencyKey } : {})
        },
        body: body ? JSON.stringify(body) : undefined
      });
    } catch (networkError) {
      response = null; // Netzwerkfehler → erneut versuchen
    }
    if (response && response.status !== 429 && response.status < 500) {
      const json = response.status === 204 ? {} : await response.json();
      if (!response.ok) {
        // 4xx: nicht wiederholen, sondern Eingaben/Rechte prüfen
        throw new Error(`${json.error.message} [${response.status}, ${json.error.request_id}]`);
      }
      return json;
    }
    const retryAfter = Number(response?.headers.get('Retry-After')) || 2 ** (attempt - 1);
    await new Promise((r) => setTimeout(r, Math.min(retryAfter, 30) * 1000));
  }
  throw new Error('Persolz ist gerade nicht erreichbar – bitte später erneut versuchen.');
}

const kontakt = await persolz('POST', '/contacts', { email: 'anna@beispiel.de', list_ids: [3] });

Häufige Fehler und ihre Lösung

  • 401 obwohl der Schlüssel stimmt: Manche Server (z. B. Apache mit CGI) entfernen den Authorization-Header auf Deiner Seite, bevor Dein Code ihn sendet – prüfe, ob er wirklich mitgeht, oder nutze X-API-Key.
  • 402 plan_required: Die API oder die Funktion (z. B. KI, A/B-Tests) ist im Tarif nicht enthalten → Persolz Pro.
  • 402 limit_reached: Ein Tariflimit ist erreicht (Kontakte, Tagesversände, Speicher …) – Details in der Meldung, Überblick mit GET /plan/limits.
  • 403 forbidden: Scope fehlt, IP oder Origin nicht freigegeben (reason verrät es) oder das Konto ist gesperrt.
  • 404 not_found: Die ID gibt es nicht – oder sie gehört zu einem anderen Konto.
  • 409 conflict: Der Zustand passt nicht (z. B. Newsletter schon versendet, Adresse schon gesperrt, Idempotency-Key mit anderem Inhalt).
  • 422 validation_error: In error.fields steht je Feld, was nicht passt.

API-Referenz

Alle Endpunkte im Überblick

Jeder Endpunkt mit Parametern, Antwortfeldern, möglichen Fehlern und Beispielen in cURL, PHP, JavaScript und Python. Alle Pfade beziehen sich auf https://persolz.com/api/v1.

System2 API-Index und die OpenAPI-Beschreibung (öffentlich). Konto & Tarif2 Dein Konto, Tarif, Limits, Verbrauch und KI-Guthaben. E-Mail-Listen9 Listen anlegen, ändern, löschen und Mitglieder verwalten. Kontakte11 Kontakte suchen, anlegen (Upsert per E-Mail), ändern, abmelden, Massenabgleich und Aktivitäten. Abmeldungen4 Gesperrte Adressen (Abmeldeliste) lesen, ergänzen und aufheben. E-Mails3 Einzel-E-Mails (z. B. Transaktions- oder Follow-up-Mails) versenden oder planen. Newsletter10 Newsletter als Entwurf anlegen, testen, versenden, planen und auswerten. Seinfeld-Mails15 Tägliche Story-Mails: Einstellungen, Themen, Entwürfe, Planung und KI-Generierung. Seifenopern15 Story-Sequenzen (Soap-Opera-Sequenzen): Tage, Teilnehmer, Aktivierung und KI-Gliederung. Vorlagen5 Vorlagen-Bibliothek und eigene E-Mail-Vorlagen. Anhänge4 Dateien in der Anhang-Cloud hochladen, auflisten und löschen. SMTP-Server2 Verbundene SMTP-Konten (ohne Passwörter) und Verbindungstest. Kampagnen & Analytics4 Alle Versände mit Status und Kennzahlen, Gesamtauswertung. A/B-Tests4 A/B-Tests anlegen, Ergebnisse lesen und den Gewinner festlegen. Termine5 Terminbuch: Termine anlegen, ändern und abfragen. Posteingang16 Verbundene IMAP-Postfächer: Mails lesen und sortieren, antworten, KI-Entwürfe schreiben lassen und freigeben, Kennzahlen abrufen. KI2 KI-Texte erzeugen und den KI-Verbrauch abfragen. Webhooks8 Webhooks verwalten, testen und Zustellungen einsehen. Protokolle2 Aktivitätsprotokoll und API-Protokoll Deines Kontos. Öffentliche Anmeldung2 Anmeldeformulare (z. B. aus Deinem Funnel) – ohne API-Schlüssel. Integrationen2 Zahlungsanbieter (Digistore24, CopeCart) übertragen Käufer automatisch in Persolz.

API-Referenz

System

API-Index und die OpenAPI-Beschreibung (öffentlich).

Endpunkte2

API-Index

GET /api/v1
Öffentlich

Name, Version und Links zur Dokumentation und zur OpenAPI-Beschreibung. Ohne API-Schlüssel abrufbar.

Antwort

200 OK

Antwortfelder
FeldBeschreibung
name string Name der API.
version string Version der API.
base_url string Basis-URL der API.
docs_url string Link zu dieser Dokumentation.
openapi_url string Link zur OpenAPI-Beschreibung.
time string (Datum/Zeit) Aktuelle Serverzeit (Europe/Berlin).
GET /
curl https://persolz.com/api/v1
<?php
$ch = curl_init('https://persolz.com/api/v1');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1');
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1",
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "name": "Persolz REST API",
    "version": "1.1.0",
    "base_url": "https://persolz.com/api/v1",
    "docs_url": "https://persolz.com/api-docs",
    "openapi_url": "https://persolz.com/api/v1/openapi.json",
    "time": "2026-10-09T14:05:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

OpenAPI-Beschreibung

GET /api/v1/openapi.json
Öffentlich

Die vollständige, maschinenlesbare Beschreibung dieser API im Format OpenAPI 3.1 (z. B. für Postman, Insomnia, Code-Generatoren). Ohne Umschlag {data}.

Antwort

200 OK

Antwortfelder
FeldBeschreibung
openapi string Version der OpenAPI-Spezifikation (3.1.0).
info object Titel, Version, Beschreibung und Kontakt der API.
info.title string Titel.
info.version string Version der API.
paths object Alle Endpunkte mit Parametern, Request-Bodys und Antworten.
GET /openapi.json
curl https://persolz.com/api/v1/openapi.json
<?php
$ch = curl_init('https://persolz.com/api/v1/openapi.json');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$spec = json_decode($body, true);
echo $spec['info']['title'] . ' ' . $spec['info']['version'];
const response = await fetch('https://persolz.com/api/v1/openapi.json');
const spec = await response.json();
console.log(spec.info.title, spec.info.version);
import requests

response = requests.get(
    "https://persolz.com/api/v1/openapi.json",
    timeout=30,
)
spec = response.json()
print(spec["info"]["title"], spec["info"]["version"])
Antwort 200 JSON
{
  "openapi": "3.1.0",
  "info": {
    "title": "Persolz REST API",
    "version": "1.1.0"
  },
  "paths": {}
}

API-Referenz

Konto & Tarif

Dein Konto, Tarif, Limits, Verbrauch und KI-Guthaben.

Endpunkte2

Konto, Tarif & Verbrauch

GET /api/v1/me
Scope read

Dein Konto, der aktive Tarif mit Limits und Funktionen, der aktuelle Verbrauch (Kontakte, Listen, Speicher, heutige Versände …), das KI-Guthaben und der verwendete API-Schlüssel.

Antwort

200 OK

Alle 55 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
account object Dein Konto.
account.id integer ID Deines Kontos.
account.email string E-Mail-Adresse.
account.first_name string Vorname.
account.last_name string Nachname.
account.company string Firma.
account.timezone string Zeitzone des Kontos.
account.email_verified boolean E-Mail-Adresse bestätigt.
account.created_at string (Datum/Zeit) Registriert am.
plan object Dein Tarif.
plan.key string Tarif-Schlüssel: free, trial oder pro.
plan.label string Tarif in Worten.
plan.trial_days_left integer Verbleibende Tage der Testphase.
plan.trial_ends_at string · null Ende der Testphase oder null.
plan.subscription_status string Status des Abos (z. B. active) oder null.
features object Funktionen des Tarifs (true = freigeschaltet).
features.ai boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
features.api boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
features.seinfeld boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
features.soap boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
features.newsletter boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
features.email boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
features.ab_tests boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
features.calendar boolean true, wenn diese Funktion in Deinem Tarif enthalten ist.
limits object Limits des Tarifs (-1 = unbegrenzt).
limits.max_lists integer Limit (-1 = unbegrenzt).
limits.max_contacts integer Limit (-1 = unbegrenzt).
limits.emails_per_day integer Limit (-1 = unbegrenzt).
limits.storage_mb integer Limit (-1 = unbegrenzt).
usage object Aktueller Verbrauch.
usage.contacts integer Anzahl Kontakte.
usage.lists integer Anzahl Listen.
usage.soap_sequences integer Anzahl Seifenopern.
usage.smtp_accounts integer Verbundene SMTP-Konten.
usage.custom_templates integer Eigene Vorlagen.
usage.storage_bytes integer Belegter Speicher der Anhang-Cloud in Bytes.
usage.active_ab_tests integer Laufende A/B-Tests.
usage.today object Heute gestartete Versände je Typ (für die Tageslimits).
usage.today.email integer Heute gestartete Versände dieses Typs.
usage.today.newsletter integer Heute gestartete Versände dieses Typs.
usage.today.seinfeld integer Heute gestartete Versände dieses Typs.
ai object KI-Guthaben und -Verbrauch im laufenden Monat.
ai.credit number Monatliches KI-Guthaben in Euro.
ai.used number Im Monat verbraucht (Euro).
ai.remaining number Verbleibendes KI-Guthaben (Euro).
ai.requests integer KI-Anfragen im laufenden Monat.
ai.limit integer Max. KI-Anfragen pro Monat (0 = unbegrenzt).
ai.period string Abrechnungsmonat (YYYY-MM).
ai.allowed boolean KI ist für Dich gerade nutzbar (Tarif und Guthaben).
api_key object Der API-Schlüssel dieser Anfrage.
api_key.id integer ID des Schlüssels.
api_key.name string Name des Schlüssels.
api_key.prefix string Anfang des Schlüssels (zum Wiedererkennen).
api_key.scopes array<string> Berechtigungen (["*"] = alle).
api_key.rate_limit integer Erlaubte Anfragen pro Minute.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /me
curl https://persolz.com/api/v1/me \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/me');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/me', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/me",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "account": {
      "id": 2,
      "email": "du@beispiel.de",
      "first_name": "Daniel",
      "last_name": "Scholz",
      "company": "Kartenherz",
      "timezone": "Europe/Berlin",
      "email_verified": true,
      "created_at": "2026-09-01T10:00:00+02:00"
    },
    "plan": {
      "key": "pro",
      "label": "Pro",
      "trial_days_left": 0,
      "trial_ends_at": null,
      "subscription_status": "active"
    },
    "features": {
      "ai": true,
      "api": true,
      "seinfeld": true,
      "soap": true,
      "newsletter": true,
      "email": true,
      "ab_tests": true,
      "calendar": true
    },
    "limits": {
      "max_lists": -1,
      "max_contacts": -1,
      "emails_per_day": -1,
      "storage_mb": 2000
    },
    "usage": {
      "contacts": 1250,
      "lists": 3,
      "soap_sequences": 2,
      "smtp_accounts": 1,
      "custom_templates": 4,
      "storage_bytes": 5242880,
      "active_ab_tests": 1,
      "today": {
        "email": 3,
        "newsletter": 1,
        "seinfeld": 1
      }
    },
    "ai": {
      "credit": 10.0,
      "used": 2.31,
      "remaining": 7.69,
      "requests": 12,
      "limit": 0,
      "period": "2026-10",
      "allowed": true
    },
    "api_key": {
      "id": 5,
      "name": "Funnel kartenherz.com",
      "prefix": "pz_live_AbCdEf",
      "scopes": ["*"],
      "rate_limit": 120
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Tarif-Limits mit Verbrauch

GET /api/v1/plan/limits
Scope read

Alle Limits des Tarifs mit aktuellem Verbrauch und Rest (-1 = unbegrenzt) sowie alle Funktionen mit Freischaltung.

Antwort

200 OK

Alle 20 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
plan string Tarif-Schlüssel: free, trial oder pro.
plan_label string Tarif in Worten.
limits object Limits des Tarifs (-1 = unbegrenzt).
limits.max_contacts object Limit mit Verbrauch und Rest.
limits.max_contacts.label string Name des Limits.
limits.max_contacts.limit integer Limit des Tarifs (-1 = unbegrenzt).
limits.max_contacts.used integer Aktueller Verbrauch.
limits.max_contacts.remaining integer Noch verfügbar (-1 = unbegrenzt).
limits.emails_per_day object Limit mit Verbrauch und Rest.
limits.emails_per_day.label string Name des Limits.
limits.emails_per_day.limit integer Limit des Tarifs (-1 = unbegrenzt).
limits.emails_per_day.used integer Aktueller Verbrauch.
limits.emails_per_day.remaining integer Noch verfügbar (-1 = unbegrenzt).
features object Funktionen des Tarifs (true = freigeschaltet).
features.api object Funktion mit Name und Freischaltung.
features.api.label string Name der Funktion.
features.api.enabled boolean Funktion ist freigeschaltet.
features.ai object Funktion mit Name und Freischaltung.
features.ai.label string Name der Funktion.
features.ai.enabled boolean Funktion ist freigeschaltet.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /plan/limits
curl https://persolz.com/api/v1/plan/limits \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/plan/limits');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/plan/limits', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/plan/limits",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "plan": "pro",
    "plan_label": "Pro",
    "limits": {
      "max_contacts": {
        "label": "Kontakte",
        "limit": -1,
        "used": 1250,
        "remaining": -1
      },
      "emails_per_day": {
        "label": "E-Mails pro Tag",
        "limit": -1,
        "used": 3,
        "remaining": -1
      }
    },
    "features": {
      "api": {
        "label": "API & Webhooks",
        "enabled": true
      },
      "ai": {
        "label": "KI-Assistent",
        "enabled": true
      }
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Das Listen-Objekt

Eine E-Mail-Liste mit Mitgliederzahlen und den öffentlichen Anmelde-Adressen.

Attribute

Das Listen-Objekt
FeldBeschreibung
id integer Eindeutige ID.
name string Name der Liste (eindeutig in Deinem Konto).
description string Interne Beschreibung.
color string Farbe als Hex-Wert (#RRGGBB).
double_optin boolean Neue Anmeldungen müssen per Bestätigungsmail bestätigt werden (Double-Opt-in).
welcome_soap_id integer Seifenoper, die beim Eintrag automatisch startet, oder null.
doi_redirect_url string Seite nach erfolgreicher Bestätigung oder null.
redirect_domains array<string> Erlaubte Weiterleitungs-Domains für success_url/error_url des Anmeldeformulars (leer = alle).
public_token string Öffentlicher Token für Anmeldeformulare (POST /subscribe/{token}).
subscribe_url string Öffentliche Anmelde-URL – ohne API-Schlüssel nutzbar.
form_url string Fertiges HTML-Anmeldeformular zum Verlinken oder Einbetten (iframe).
counts object Mitglieder nach Status.
counts.subscribed integer Aktive Mitglieder.
counts.pending integer Noch unbestätigte Anmeldungen (Double-Opt-in).
counts.unsubscribed integer Abgemeldete Mitglieder.
counts.bounced integer Unzustellbare Adressen.
counts.total integer Alle Mitglieder.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Listen-Objekt JSON
{
  "id": 3,
  "name": "Kartenherz Funnel",
  "description": "Leads aus dem Funnel auf kartenherz.com",
  "color": "#5B3DF5",
  "double_optin": true,
  "welcome_soap_id": 7,
  "doi_redirect_url": "https://kartenherz.com/danke",
  "redirect_domains": ["kartenherz.com"],
  "public_token": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
  "subscribe_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
  "form_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form",
  "counts": {
    "subscribed": 1180,
    "pending": 24,
    "unsubscribed": 41,
    "bounced": 5,
    "total": 1250
  },
  "created_at": "2026-09-01T10:00:00+02:00",
  "updated_at": "2026-10-01T08:30:00+02:00"
}

Listen auflisten

GET /api/v1/lists
Scope read paginiert

Alle E-Mail-Listen mit Mitgliederzahlen je Status (subscribed, pending, unsubscribed, bounced, total), dem öffentlichen Anmelde-Token und der Anmelde-URL.

Query-Parameter

NameBeschreibung
q string optional Suche im Namen. Beispiel: funnel
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Listen-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /lists
curl https://persolz.com/api/v1/lists \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/lists",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 3,
      "name": "Kartenherz Funnel",
      "description": "Leads aus dem Funnel auf kartenherz.com",
      "color": "#5B3DF5",
      "double_optin": true,
      "welcome_soap_id": 7,
      "doi_redirect_url": "https://kartenherz.com/danke",
      "redirect_domains": ["kartenherz.com"],
      "public_token": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
      "subscribe_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
      "form_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form",
      "counts": {
        "subscribed": 1180,
        "pending": 24,
        "unsubscribed": 41,
        "bounced": 5,
        "total": 1250
      },
      "created_at": "2026-09-01T10:00:00+02:00",
      "updated_at": "2026-10-01T08:30:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Liste anlegen

POST /api/v1/lists
Scope write Idempotency-Key

Legt eine neue Liste an. Im Free-Tarif ist nur eine Liste möglich (→ 402 limit_reached).

Request-Body application/json

FeldBeschreibung
name string Pflicht Name der Liste (eindeutig, max. 190 Zeichen).
description string optional Beschreibung (intern).
color string optional Farbe als Hex-Wert (#RRGGBB).
double_optin boolean optional Neue Anmeldungen über Formular/API müssen per Bestätigungsmail bestätigt werden.
welcome_soap_id integer optional Seifenoper, die beim Eintrag automatisch startet (null = keine).
doi_redirect_url string optional Seite nach erfolgreicher Bestätigung (https://…).
redirect_domains array optional Erlaubte Weiterleitungs-Domains für success_url/error_url des öffentlichen Anmeldeformulars (inkl. Subdomains; Liste oder kommagetrennt, max. 20). Leer = jedes http(s)-Ziel ist erlaubt.

Antwort

201 Liste angelegt – ein Listen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited

POST /lists
curl -X POST https://persolz.com/api/v1/lists \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Kartenherz Funnel",
    "description": "Leads aus dem Funnel auf kartenherz.com",
    "double_optin": true,
    "welcome_soap_id": 7,
    "doi_redirect_url": "https://kartenherz.com/danke"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'name' => 'Kartenherz Funnel',
        'description' => 'Leads aus dem Funnel auf kartenherz.com',
        'double_optin' => true,
        'welcome_soap_id' => 7,
        'doi_redirect_url' => 'https://kartenherz.com/danke',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Kartenherz Funnel',
    description: 'Leads aus dem Funnel auf kartenherz.com',
    double_optin: true,
    welcome_soap_id: 7,
    doi_redirect_url: 'https://kartenherz.com/danke'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/lists",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "name": "Kartenherz Funnel",
        "description": "Leads aus dem Funnel auf kartenherz.com",
        "double_optin": True,
        "welcome_soap_id": 7,
        "doi_redirect_url": "https://kartenherz.com/danke",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 3,
    "name": "Kartenherz Funnel",
    "description": "Leads aus dem Funnel auf kartenherz.com",
    "color": "#5B3DF5",
    "double_optin": true,
    "welcome_soap_id": 7,
    "doi_redirect_url": "https://kartenherz.com/danke",
    "redirect_domains": ["kartenherz.com"],
    "public_token": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "subscribe_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "form_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form",
    "counts": {
      "subscribed": 1180,
      "pending": 24,
      "unsubscribed": 41,
      "bounced": 5,
      "total": 1250
    },
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Liste abrufen

GET /api/v1/lists/{id}
Scope read

Eine Liste mit Zahlen, Token und Anmelde-URLs.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Listen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /lists/{id}
curl https://persolz.com/api/v1/lists/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/lists/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 3,
    "name": "Kartenherz Funnel",
    "description": "Leads aus dem Funnel auf kartenherz.com",
    "color": "#5B3DF5",
    "double_optin": true,
    "welcome_soap_id": 7,
    "doi_redirect_url": "https://kartenherz.com/danke",
    "redirect_domains": ["kartenherz.com"],
    "public_token": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "subscribe_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "form_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form",
    "counts": {
      "subscribed": 1180,
      "pending": 24,
      "unsubscribed": 41,
      "bounced": 5,
      "total": 1250
    },
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Liste ändern

PATCH /api/v1/lists/{id}
Scope write

Ändert nur die übergebenen Felder.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
name string optional Name der Liste (eindeutig, max. 190 Zeichen).
description string optional Beschreibung (intern).
color string optional Farbe als Hex-Wert (#RRGGBB).
double_optin boolean optional Neue Anmeldungen über Formular/API müssen per Bestätigungsmail bestätigt werden.
welcome_soap_id integer optional Seifenoper, die beim Eintrag automatisch startet (null = keine).
doi_redirect_url string optional Seite nach erfolgreicher Bestätigung (https://…).
redirect_domains array optional Erlaubte Weiterleitungs-Domains für success_url/error_url des öffentlichen Anmeldeformulars (inkl. Subdomains; Liste oder kommagetrennt, max. 20). Leer = jedes http(s)-Ziel ist erlaubt.

Antwort

200 OK – ein Listen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

PATCH /lists/{id}
curl -X PATCH https://persolz.com/api/v1/lists/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "description": "Leads aus Funnel und Messe",
    "double_optin": false
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'description' => 'Leads aus Funnel und Messe',
        'double_optin' => false,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    description: 'Leads aus Funnel und Messe',
    double_optin: false
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/lists/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "description": "Leads aus Funnel und Messe",
        "double_optin": False,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 3,
    "name": "Kartenherz Funnel",
    "description": "Leads aus dem Funnel auf kartenherz.com",
    "color": "#5B3DF5",
    "double_optin": true,
    "welcome_soap_id": 7,
    "doi_redirect_url": "https://kartenherz.com/danke",
    "redirect_domains": ["kartenherz.com"],
    "public_token": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "subscribe_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "form_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form",
    "counts": {
      "subscribed": 1180,
      "pending": 24,
      "unsubscribed": 41,
      "bounced": 5,
      "total": 1250
    },
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Liste ersetzen

PUT /api/v1/lists/{id}
Scope write

Setzt alle Felder neu (nicht übergebene optionale Felder werden geleert, double_optin wird false). „name“ ist Pflicht.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
name string Pflicht Name der Liste (eindeutig, max. 190 Zeichen).
description string optional Beschreibung (intern).
color string optional Farbe als Hex-Wert (#RRGGBB).
double_optin boolean optional Neue Anmeldungen über Formular/API müssen per Bestätigungsmail bestätigt werden.
welcome_soap_id integer optional Seifenoper, die beim Eintrag automatisch startet (null = keine).
doi_redirect_url string optional Seite nach erfolgreicher Bestätigung (https://…).
redirect_domains array optional Erlaubte Weiterleitungs-Domains für success_url/error_url des öffentlichen Anmeldeformulars (inkl. Subdomains; Liste oder kommagetrennt, max. 20). Leer = jedes http(s)-Ziel ist erlaubt.

Antwort

200 OK – ein Listen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

PUT /lists/{id}
curl -X PUT https://persolz.com/api/v1/lists/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Kartenherz Funnel",
    "description": "Leads aus dem Funnel auf kartenherz.com",
    "double_optin": true,
    "welcome_soap_id": 7,
    "doi_redirect_url": "https://kartenherz.com/danke"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'name' => 'Kartenherz Funnel',
        'description' => 'Leads aus dem Funnel auf kartenherz.com',
        'double_optin' => true,
        'welcome_soap_id' => 7,
        'doi_redirect_url' => 'https://kartenherz.com/danke',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists/42', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Kartenherz Funnel',
    description: 'Leads aus dem Funnel auf kartenherz.com',
    double_optin: true,
    welcome_soap_id: 7,
    doi_redirect_url: 'https://kartenherz.com/danke'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.put(
    "https://persolz.com/api/v1/lists/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "name": "Kartenherz Funnel",
        "description": "Leads aus dem Funnel auf kartenherz.com",
        "double_optin": True,
        "welcome_soap_id": 7,
        "doi_redirect_url": "https://kartenherz.com/danke",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 3,
    "name": "Kartenherz Funnel",
    "description": "Leads aus dem Funnel auf kartenherz.com",
    "color": "#5B3DF5",
    "double_optin": true,
    "welcome_soap_id": 7,
    "doi_redirect_url": "https://kartenherz.com/danke",
    "redirect_domains": ["kartenherz.com"],
    "public_token": "9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "subscribe_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    "form_url": "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form",
    "counts": {
      "subscribed": 1180,
      "pending": 24,
      "unsubscribed": 41,
      "bounced": 5,
      "total": 1250
    },
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Liste löschen

DELETE /api/v1/lists/{id}
Scope write

Löscht die Liste. Kontakte bleiben erhalten – mit ?delete_contacts=true werden Kontakte gelöscht, die in keiner anderen Liste sind.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
delete_contacts boolean optional Kontakte ohne andere Liste ebenfalls löschen. Beispiel: false

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /lists/{id}
curl -X DELETE https://persolz.com/api/v1/lists/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/lists/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/lists/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Mitglieder einer Liste

GET /api/v1/lists/{id}/contacts
Scope read paginiert

Kontakte der Liste, filterbar nach Status und Suchbegriff.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
status string optional subscribed | pending | unsubscribed | bounced Werte: subscribed, pending, unsubscribed, bounced Beispiel: subscribed
q string optional Suche in Name, E-Mail, Telefon, Ort. Beispiel: anna
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Kontakt-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /lists/{id}/contacts
curl "https://persolz.com/api/v1/lists/42/contacts?status=subscribed" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'subscribed',
]);
$ch = curl_init('https://persolz.com/api/v1/lists/42/contacts?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'subscribed'
});
const response = await fetch('https://persolz.com/api/v1/lists/42/contacts?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/lists/42/contacts",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "subscribed",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 42,
      "email": "anna@beispiel.de",
      "first_name": "Anna",
      "last_name": "Schmidt",
      "address": "Lindenweg 3",
      "zip": "10115",
      "city": "Berlin",
      "phone": "+49 30 1234567",
      "note": "Kam über den Kartenherz-Funnel",
      "tags": ["funnel", "kartenherz"],
      "status": "subscribed",
      "source": "api",
      "custom_fields": {
        "kundennummer": "K-1001"
      },
      "doi_confirmed_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00",
      "lists": [
        {
          "id": 3,
          "name": "Kartenherz Funnel",
          "color": "#5B3DF5"
        }
      ]
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Kontakte zur Liste hinzufügen

POST /api/v1/lists/{id}/contacts
Scope write Idempotency-Key

Fügt bestehende Kontakte (contact_ids) hinzu und/oder legt Kontakte per E-Mail an bzw. aktualisiert sie (contacts – Upsert, max. 1000). Bei Double-Opt-in-Listen erhalten neue Kontakte eine Bestätigungsmail. Automationen der Liste (Willkommens-Seifenoper) starten für aktive Kontakte.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
contact_ids array<integer> optional IDs bestehender Kontakte.
contacts array<object> optional Kontakte als Objekte (email Pflicht, weitere Felder wie bei POST /contacts).

Antwort

200 OK

Alle 14 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
added integer Neu in die Liste aufgenommene Kontakte.
created integer Neu angelegte Kontakte.
updated integer Aktualisierte bestehende Kontakte.
skipped integer Übersprungene Einträge (z. B. bereits Mitglied).
failed integer Fehlgeschlagene Einträge (Details in errors).
errors array Fehler je Eintrag (leer, wenn alles geklappt hat).
list object Die Liste mit den neuen Mitgliederzahlen.
list.id integer ID der Liste.
list.counts object Mitglieder nach Status.
list.counts.subscribed integer Aktive Mitglieder.
list.counts.pending integer Noch unbestätigte Anmeldungen (Double-Opt-in).
list.counts.unsubscribed integer Abgemeldete Mitglieder.
list.counts.bounced integer Unzustellbare Adressen.
list.counts.total integer Alle Mitglieder.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /lists/{id}/contacts
curl -X POST https://persolz.com/api/v1/lists/42/contacts \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_ids": [42, 43],
    "contacts": [
      {
        "email": "ben@beispiel.de",
        "first_name": "Ben",
        "tags": ["messe"]
      }
    ]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists/42/contacts');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'contact_ids' => [42, 43],
        'contacts' => [
            [
                'email' => 'ben@beispiel.de',
                'first_name' => 'Ben',
                'tags' => ['messe'],
            ],
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/lists/42/contacts', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    contact_ids: [42, 43],
    contacts: [
      {
        email: 'ben@beispiel.de',
        first_name: 'Ben',
        tags: ['messe']
      }
    ]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/lists/42/contacts",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "contact_ids": [42, 43],
        "contacts": [
            {
                "email": "ben@beispiel.de",
                "first_name": "Ben",
                "tags": ["messe"],
            },
        ],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "added": 2,
    "created": 1,
    "updated": 0,
    "skipped": 0,
    "failed": 0,
    "errors": [],
    "list": {
      "id": 3,
      "counts": {
        "subscribed": 1182,
        "pending": 25,
        "unsubscribed": 41,
        "bounced": 5,
        "total": 1253
      }
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kontakt aus Liste entfernen

DELETE /api/v1/lists/{id}/contacts/{contactId}
Scope write

Entfernt die Mitgliedschaft (der Kontakt selbst bleibt erhalten).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource
contactId integer Pflicht ID des Kontakts

Antwort

204 Entfernt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /lists/{id}/contacts/{contactId}
curl -X DELETE https://persolz.com/api/v1/lists/42/contacts/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/lists/42/contacts/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/lists/42/contacts/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/lists/42/contacts/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

API-Referenz

Kontakte

Kontakte suchen, anlegen (Upsert per E-Mail), ändern, abmelden, Massenabgleich und Aktivitäten.

Ein Kontakt ist eindeutig über seine E-Mail-Adresse. POST /contacts ist deshalb ein Upsert: neu anlegen oder vorhandenen Kontakt ergänzen. Für viele Kontakte auf einmal nutze POST /contacts/bulk.

Objekte: Kontakt-Objekt

Das Kontakt-Objekt

Ein Kontakt mit Stammdaten, Tags, eigenen Feldern und seinen Listen.

Attribute

Das Kontakt-Objekt
FeldBeschreibung
id integer Eindeutige ID.
email string E-Mail-Adresse (eindeutig je Konto, kleingeschrieben).
first_name string Vorname.
last_name string Nachname.
address string Straße und Hausnummer.
zip string Postleitzahl.
city string Ort.
phone string Telefonnummer.
note string Notiz zum Kontakt.
tags array<string> Tags des Kontakts.
status string subscribed (aktiv), pending (unbestätigt), unsubscribed (abgemeldet) oder bounced (unzustellbar).
source string Herkunft: manual, import, form, api, digistore, copecart …
custom_fields object Eigene Felder als Objekt – jeder Schlüssel ist auch als Platzhalter nutzbar ({{kundennummer}}).
custom_fields.kundennummer string Wert eines eigenen Feldes (hier ein Beispiel).
doi_confirmed_at string · null Zeitpunkt der Double-Opt-in-Bestätigung oder null.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
lists array<object> Listen, in denen der Kontakt ist.
lists[].id integer ID der Liste.
lists[].name string Name der Liste.
lists[].color string Farbe der Liste.
Das Kontakt-Objekt JSON
{
  "id": 42,
  "email": "anna@beispiel.de",
  "first_name": "Anna",
  "last_name": "Schmidt",
  "address": "Lindenweg 3",
  "zip": "10115",
  "city": "Berlin",
  "phone": "+49 30 1234567",
  "note": "Kam über den Kartenherz-Funnel",
  "tags": ["funnel", "kartenherz"],
  "status": "subscribed",
  "source": "api",
  "custom_fields": {
    "kundennummer": "K-1001"
  },
  "doi_confirmed_at": null,
  "created_at": "2026-10-09T14:05:00+02:00",
  "updated_at": "2026-10-09T14:05:00+02:00",
  "lists": [
    {
      "id": 3,
      "name": "Kartenherz Funnel",
      "color": "#5B3DF5"
    }
  ]
}

Kontakte suchen

GET /api/v1/contacts
Scope read paginiert

Kontakte mit Listen, gefiltert und sortiert. Für eine Synchronisation eignet sich updated_after + sort=updated_asc.

Query-Parameter

NameBeschreibung
email string optional Exakte E-Mail-Adresse. Beispiel: anna@beispiel.de
q string optional Suche in Name, E-Mail, Telefon, Ort, PLZ. Beispiel: schmidt
list_id integer optional Nur Mitglieder dieser Liste. Beispiel: 3
status string optional subscribed | pending | unsubscribed | bounced Werte: subscribed, pending, unsubscribed, bounced Beispiel: subscribed
tag string optional Nur Kontakte mit diesem Tag. Beispiel: funnel
source string optional Herkunft: manual, import, form, api, digistore, copecart … Beispiel: api
created_after string optional Angelegt nach (ISO 8601 oder YYYY-MM-DD). Beispiel: 2026-10-01
updated_after string optional Geändert nach (ISO 8601). Beispiel: 2026-10-09T00:00:00+02:00
sort string optional created_desc (Standard) | created_asc | updated_desc | updated_asc | email_asc | name_asc Werte: created_desc, created_asc, updated_desc, updated_asc, email_asc, name_asc Beispiel: created_desc
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Kontakt-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /contacts
curl "https://persolz.com/api/v1/contacts?list_id=3&status=subscribed&per_page=50" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'list_id' => '3',
    'status' => 'subscribed',
    'per_page' => '50',
]);
$ch = curl_init('https://persolz.com/api/v1/contacts?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  list_id: '3',
  status: 'subscribed',
  per_page: '50'
});
const response = await fetch('https://persolz.com/api/v1/contacts?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/contacts",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "list_id": "3",
        "status": "subscribed",
        "per_page": "50",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 42,
      "email": "anna@beispiel.de",
      "first_name": "Anna",
      "last_name": "Schmidt",
      "address": "Lindenweg 3",
      "zip": "10115",
      "city": "Berlin",
      "phone": "+49 30 1234567",
      "note": "Kam über den Kartenherz-Funnel",
      "tags": ["funnel", "kartenherz"],
      "status": "subscribed",
      "source": "api",
      "custom_fields": {
        "kundennummer": "K-1001"
      },
      "doi_confirmed_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00",
      "lists": [
        {
          "id": 3,
          "name": "Kartenherz Funnel",
          "color": "#5B3DF5"
        }
      ]
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Kontakt anlegen oder aktualisieren (Upsert)

POST /api/v1/contacts
Scope write Idempotency-Key

Legt einen Kontakt an oder aktualisiert ihn anhand der E-Mail-Adresse (nur übergebene, nicht leere Werte werden übernommen, Tags ergänzt, Listen hinzugefügt). Double-Opt-in-Listen lösen eine Bestätigungsmail aus (Status pending). Optional startet soap_sequence_id eine Seifenoper (nur für aktive Kontakte). 201 = neu angelegt, 200 = aktualisiert.

Request-Body application/json

FeldBeschreibung
email string Pflicht E-Mail-Adresse (eindeutig je Konto, wird kleingeschrieben).
first_name string optional Vorname (Alias: vorname, firstName).
last_name string optional Nachname (Alias: nachname, lastName).
name string optional Voller Name – wird aufgeteilt, wenn Vor-/Nachname fehlen.
phone string optional Telefon.
address string optional Straße und Hausnummer.
zip string optional PLZ.
city string optional Ort.
note string optional Notiz (max. 5000 Zeichen).
tags array<string> optional Tags (Array oder kommagetrennt). Beim Upsert werden Tags ergänzt.
custom_fields object optional Eigene Felder (max. 50 Schlüssel a–z, 0–9, _ und -) – auch als Platzhalter {{schluessel}} nutzbar.
status string optional subscribed | pending | unsubscribed. Abgemeldete Adressen werden nur mit consent=true reaktiviert. Werte: subscribed, pending, unsubscribed
list_ids array<integer> optional Listen, in die der Kontakt aufgenommen wird (Double-Opt-in der Liste wird beachtet). Aliase: lists (IDs oder Listen-Objekte wie in der Antwort), list_id.
consent boolean optional Bestätigt eine neue Einwilligung (nötig, um abgemeldete Kontakte wieder anzumelden).
doi boolean optional Erzwingt eine Bestätigungsmail (Double-Opt-in), auch wenn die Liste es nicht verlangt.
soap_sequence_id integer optional Seifenoper, in die der Kontakt aufgenommen wird.
soap_start_date string optional Startdatum der Seifenoper (YYYY-MM-DD, Standard heute).

Antwort

201 Angelegt (200 bei Aktualisierung) – ein Kontakt-Objekt, zusätzlich:

Alle 10 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
result object Was beim Upsert passiert ist.
result.created boolean Der Kontakt wurde neu angelegt.
result.updated boolean Ein bestehender Kontakt wurde aktualisiert.
result.status string Status des Kontakts nach dem Upsert.
result.doi_sent boolean Eine Bestätigungsmail (Double-Opt-in) wurde gesendet.
result.message string · null Hinweis zum Ergebnis oder null.
result.soap_enrollment object Ergebnis der Aufnahme in die Seifenoper (nur mit soap_sequence_id) oder null.
result.soap_enrollment.ok boolean Aufnahme erfolgreich.
result.soap_enrollment.enrollment_id integer ID der Teilnahme.
result.soap_enrollment.error string · null Grund, falls die Aufnahme nicht möglich war (z. B. Double-Opt-in noch offen).

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited

POST /contacts
curl -X POST https://persolz.com/api/v1/contacts \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "tags": ["funnel"],
    "list_ids": [3],
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "soap_sequence_id": 7
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'email' => 'anna@beispiel.de',
        'first_name' => 'Anna',
        'last_name' => 'Schmidt',
        'tags' => ['funnel'],
        'list_ids' => [3],
        'custom_fields' => [
            'kundennummer' => 'K-1001',
        ],
        'soap_sequence_id' => 7,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    email: 'anna@beispiel.de',
    first_name: 'Anna',
    last_name: 'Schmidt',
    tags: ['funnel'],
    list_ids: [3],
    custom_fields: {
      kundennummer: 'K-1001'
    },
    soap_sequence_id: 7
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/contacts",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "email": "anna@beispiel.de",
        "first_name": "Anna",
        "last_name": "Schmidt",
        "tags": ["funnel"],
        "list_ids": [3],
        "custom_fields": {
            "kundennummer": "K-1001",
        },
        "soap_sequence_id": 7,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 42,
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "address": "Lindenweg 3",
    "zip": "10115",
    "city": "Berlin",
    "phone": "+49 30 1234567",
    "note": "Kam über den Kartenherz-Funnel",
    "tags": ["funnel", "kartenherz"],
    "status": "subscribed",
    "source": "api",
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "doi_confirmed_at": null,
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "lists": [
      {
        "id": 3,
        "name": "Kartenherz Funnel",
        "color": "#5B3DF5"
      }
    ],
    "result": {
      "created": true,
      "updated": false,
      "status": "subscribed",
      "doi_sent": false,
      "message": null,
      "soap_enrollment": {
        "ok": true,
        "enrollment_id": 501,
        "error": null
      }
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kontakt per E-Mail finden

GET /api/v1/contacts/lookup
Scope read

Liefert den Kontakt zur E-Mail-Adresse oder 404.

Query-Parameter

NameBeschreibung
email string optional E-Mail-Adresse. Beispiel: anna@beispiel.de

Antwort

200 OK – ein Kontakt-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

GET /contacts/lookup
curl https://persolz.com/api/v1/contacts/lookup \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/lookup');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/lookup', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/contacts/lookup",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 42,
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "address": "Lindenweg 3",
    "zip": "10115",
    "city": "Berlin",
    "phone": "+49 30 1234567",
    "note": "Kam über den Kartenherz-Funnel",
    "tags": ["funnel", "kartenherz"],
    "status": "subscribed",
    "source": "api",
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "doi_confirmed_at": null,
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "lists": [
      {
        "id": 3,
        "name": "Kartenherz Funnel",
        "color": "#5B3DF5"
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Massenabgleich (max. 1000)

POST /api/v1/contacts/bulk
Scope write Idempotency-Key

action=upsert: bis zu 1000 Kontakte anlegen/aktualisieren (optional in list_ids, mit tags). action=delete: Kontakte per ids oder emails löschen. Automationen (Seifenopern, Webhooks) laufen nur mit run_automations=true.

Request-Body application/json

FeldBeschreibung
action string Pflicht upsert | delete Werte: upsert, delete
contacts array<object> optional Für upsert: Kontakte (Felder wie POST /contacts).
ids array<integer> optional Für delete: Kontakt-IDs.
emails array<string> optional Für delete: E-Mail-Adressen.
list_ids array<integer> optional Für upsert: Listen für alle Kontakte.
tags array<string> optional Für upsert: Tags für alle Kontakte.
duplicate string optional Für upsert: update (Standard) oder skip für bestehende Adressen. Werte: update, skip
run_automations boolean optional Webhooks und Listen-Automationen auslösen (Standard false).

Antwort

200 OK

Antwortfelder
FeldBeschreibung
action string Ausgeführte Aktion: upsert oder delete.
created integer Neu angelegte Kontakte.
updated integer Aktualisierte Kontakte.
skipped integer Übersprungene Einträge (z. B. duplicate=skip).
unchanged integer Unverändert (keine neuen Daten).
failed integer Fehlgeschlagene Einträge (Details in errors).
errors array Fehler je Eintrag (leer, wenn alles geklappt hat).

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 413 payload_too_large 422 validation_error 429 rate_limited

POST /contacts/bulk
curl -X POST https://persolz.com/api/v1/contacts/bulk \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "upsert",
    "list_ids": [3],
    "tags": ["messe-2026"],
    "contacts": [
      {
        "email": "a@beispiel.de",
        "first_name": "Alex"
      },
      {
        "email": "b@beispiel.de",
        "vorname": "Bea"
      }
    ]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/bulk');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'action' => 'upsert',
        'list_ids' => [3],
        'tags' => ['messe-2026'],
        'contacts' => [
            [
                'email' => 'a@beispiel.de',
                'first_name' => 'Alex',
            ],
            [
                'email' => 'b@beispiel.de',
                'vorname' => 'Bea',
            ],
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/bulk', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    action: 'upsert',
    list_ids: [3],
    tags: ['messe-2026'],
    contacts: [
      {
        email: 'a@beispiel.de',
        first_name: 'Alex'
      },
      {
        email: 'b@beispiel.de',
        vorname: 'Bea'
      }
    ]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/contacts/bulk",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "action": "upsert",
        "list_ids": [3],
        "tags": ["messe-2026"],
        "contacts": [
            {
                "email": "a@beispiel.de",
                "first_name": "Alex",
            },
            {
                "email": "b@beispiel.de",
                "vorname": "Bea",
            },
        ],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "action": "upsert",
    "created": 2,
    "updated": 0,
    "skipped": 0,
    "unchanged": 0,
    "failed": 0,
    "errors": []
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kontakt abrufen

GET /api/v1/contacts/{id}
Scope read

Ein Kontakt mit Listen, Engagement-Kennzahlen und Seifenopern-Teilnahmen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Kontakt-Objekt, zusätzlich:

Alle 15 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
engagement object Engagement-Kennzahlen des Kontakts.
engagement.sent integer Erhaltene E-Mails.
engagement.opened integer Davon geöffnet.
engagement.clicked integer Davon mit Klick.
engagement.failed integer Nicht zustellbare E-Mails.
engagement.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
engagement.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
engagement.last_activity_at string (Datum/Zeit) Letzte Öffnung oder letzter Klick.
soap_enrollments array<object> Teilnahmen an Seifenopern.
soap_enrollments[].id integer ID der Teilnahme.
soap_enrollments[].sequence_id integer ID der Seifenoper.
soap_enrollments[].sequence_name string Name der Seifenoper.
soap_enrollments[].status string Status der Teilnahme: active, paused, completed, cancelled, unsubscribed.
soap_enrollments[].current_position integer Zuletzt erreichter Tag (Position).
soap_enrollments[].start_date string (Datum) Startdatum.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /contacts/{id}
curl https://persolz.com/api/v1/contacts/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/contacts/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 42,
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "address": "Lindenweg 3",
    "zip": "10115",
    "city": "Berlin",
    "phone": "+49 30 1234567",
    "note": "Kam über den Kartenherz-Funnel",
    "tags": ["funnel", "kartenherz"],
    "status": "subscribed",
    "source": "api",
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "doi_confirmed_at": null,
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "lists": [
      {
        "id": 3,
        "name": "Kartenherz Funnel",
        "color": "#5B3DF5"
      }
    ],
    "engagement": {
      "sent": 14,
      "opened": 11,
      "clicked": 4,
      "failed": 0,
      "open_rate": 0.7857,
      "click_rate": 0.2857,
      "last_activity_at": "2026-10-09T14:06:12+02:00"
    },
    "soap_enrollments": [
      {
        "id": 501,
        "sequence_id": 7,
        "sequence_name": "Willkommen bei Kartenherz",
        "status": "active",
        "current_position": 2,
        "start_date": "2026-10-08"
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kontakt ändern

PATCH /api/v1/contacts/{id}
Scope write

Ändert nur die übergebenen Felder. list_ids gleicht die Listen exakt ab (nicht genannte Listen werden entfernt), add_list_ids / remove_list_ids ändern einzelne Mitgliedschaften. Zum Reaktivieren abgemeldeter Kontakte ist consent=true nötig.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
email string optional E-Mail-Adresse (eindeutig je Konto, wird kleingeschrieben).
first_name string optional Vorname (Alias: vorname, firstName).
last_name string optional Nachname (Alias: nachname, lastName).
name string optional Voller Name – wird aufgeteilt, wenn Vor-/Nachname fehlen.
phone string optional Telefon.
address string optional Straße und Hausnummer.
zip string optional PLZ.
city string optional Ort.
note string optional Notiz (max. 5000 Zeichen).
tags array<string> optional Tags (Array oder kommagetrennt). Beim Upsert werden Tags ergänzt.
custom_fields object optional Eigene Felder (max. 50 Schlüssel a–z, 0–9, _ und -) – auch als Platzhalter {{schluessel}} nutzbar.
status string optional subscribed | pending | unsubscribed. Abgemeldete Adressen werden nur mit consent=true reaktiviert. Werte: subscribed, pending, unsubscribed
list_ids array<integer> optional Listen, in die der Kontakt aufgenommen wird (Double-Opt-in der Liste wird beachtet). Aliase: lists (IDs oder Listen-Objekte wie in der Antwort), list_id.
consent boolean optional Bestätigt eine neue Einwilligung (nötig, um abgemeldete Kontakte wieder anzumelden).
doi boolean optional Erzwingt eine Bestätigungsmail (Double-Opt-in), auch wenn die Liste es nicht verlangt.
add_list_ids array<integer> optional Listen hinzufügen.
remove_list_ids array<integer> optional Aus Listen entfernen.

Antwort

200 OK – ein Kontakt-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

PATCH /contacts/{id}
curl -X PATCH https://persolz.com/api/v1/contacts/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Anna-Lena",
    "tags": ["funnel", "kundin"],
    "add_list_ids": [4]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'first_name' => 'Anna-Lena',
        'tags' => ['funnel', 'kundin'],
        'add_list_ids' => [4],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    first_name: 'Anna-Lena',
    tags: ['funnel', 'kundin'],
    add_list_ids: [4]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/contacts/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "first_name": "Anna-Lena",
        "tags": ["funnel", "kundin"],
        "add_list_ids": [4],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 42,
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "address": "Lindenweg 3",
    "zip": "10115",
    "city": "Berlin",
    "phone": "+49 30 1234567",
    "note": "Kam über den Kartenherz-Funnel",
    "tags": ["funnel", "kartenherz"],
    "status": "subscribed",
    "source": "api",
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "doi_confirmed_at": null,
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "lists": [
      {
        "id": 3,
        "name": "Kartenherz Funnel",
        "color": "#5B3DF5"
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kontakt ersetzen

PUT /api/v1/contacts/{id}
Scope write

Setzt alle Profilfelder neu: nicht übergebene Felder (Name, Adresse, Notiz, Tags, eigene Felder) werden geleert. „email“ ist Pflicht. Listen werden nur mit list_ids abgeglichen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
email string Pflicht E-Mail-Adresse (eindeutig je Konto, wird kleingeschrieben).
first_name string optional Vorname (Alias: vorname, firstName).
last_name string optional Nachname (Alias: nachname, lastName).
name string optional Voller Name – wird aufgeteilt, wenn Vor-/Nachname fehlen.
phone string optional Telefon.
address string optional Straße und Hausnummer.
zip string optional PLZ.
city string optional Ort.
note string optional Notiz (max. 5000 Zeichen).
tags array<string> optional Tags (Array oder kommagetrennt). Beim Upsert werden Tags ergänzt.
custom_fields object optional Eigene Felder (max. 50 Schlüssel a–z, 0–9, _ und -) – auch als Platzhalter {{schluessel}} nutzbar.
status string optional subscribed | pending | unsubscribed. Abgemeldete Adressen werden nur mit consent=true reaktiviert. Werte: subscribed, pending, unsubscribed
list_ids array<integer> optional Listen, in die der Kontakt aufgenommen wird (Double-Opt-in der Liste wird beachtet). Aliase: lists (IDs oder Listen-Objekte wie in der Antwort), list_id.
consent boolean optional Bestätigt eine neue Einwilligung (nötig, um abgemeldete Kontakte wieder anzumelden).
doi boolean optional Erzwingt eine Bestätigungsmail (Double-Opt-in), auch wenn die Liste es nicht verlangt.

Antwort

200 OK – ein Kontakt-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

PUT /contacts/{id}
curl -X PUT https://persolz.com/api/v1/contacts/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt-Berg",
    "city": "Hamburg"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'email' => 'anna@beispiel.de',
        'first_name' => 'Anna',
        'last_name' => 'Schmidt-Berg',
        'city' => 'Hamburg',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/42', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: 'anna@beispiel.de',
    first_name: 'Anna',
    last_name: 'Schmidt-Berg',
    city: 'Hamburg'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.put(
    "https://persolz.com/api/v1/contacts/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "email": "anna@beispiel.de",
        "first_name": "Anna",
        "last_name": "Schmidt-Berg",
        "city": "Hamburg",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 42,
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "address": "Lindenweg 3",
    "zip": "10115",
    "city": "Berlin",
    "phone": "+49 30 1234567",
    "note": "Kam über den Kartenherz-Funnel",
    "tags": ["funnel", "kartenherz"],
    "status": "subscribed",
    "source": "api",
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "doi_confirmed_at": null,
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "lists": [
      {
        "id": 3,
        "name": "Kartenherz Funnel",
        "color": "#5B3DF5"
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kontakt löschen

DELETE /api/v1/contacts/{id}
Scope write

Löscht den Kontakt mit Listen-Mitgliedschaften und Seifenopern-Teilnahmen. Eine Abmeldung der Adresse bleibt als Sperre erhalten.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /contacts/{id}
curl -X DELETE https://persolz.com/api/v1/contacts/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/contacts/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/contacts/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Kontakt abmelden

POST /api/v1/contacts/{id}/unsubscribe
Scope write Idempotency-Key

Meldet den Kontakt ab: Die Adresse wird gesperrt, offene Versände und Seifenopern werden gestoppt (Webhook contact.unsubscribed).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
reason string optional Grund (optional, max. 500 Zeichen).

Antwort

200 OK – ein Kontakt-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /contacts/{id}/unsubscribe
curl -X POST https://persolz.com/api/v1/contacts/42/unsubscribe \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Wunsch per Telefon"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/42/unsubscribe');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'reason' => 'Wunsch per Telefon',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/42/unsubscribe', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    reason: 'Wunsch per Telefon'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/contacts/42/unsubscribe",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "reason": "Wunsch per Telefon",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "status": "unsubscribed",
    "id": 42,
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "address": "Lindenweg 3",
    "zip": "10115",
    "city": "Berlin",
    "phone": "+49 30 1234567",
    "note": "Kam über den Kartenherz-Funnel",
    "tags": ["funnel", "kartenherz"],
    "source": "api",
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "doi_confirmed_at": null,
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "lists": [
      {
        "id": 3,
        "name": "Kartenherz Funnel",
        "color": "#5B3DF5"
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kontakt wieder anmelden

POST /api/v1/contacts/{id}/resubscribe
Scope write Idempotency-Key

Hebt eine Abmeldung auf. Nur mit consent=true (Du bestätigst eine neue, nachweisbare Einwilligung). Mit doi=true wird stattdessen eine Bestätigungsmail gesendet.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
consent boolean Pflicht Neue Einwilligung liegt vor.
doi boolean optional Statt sofortiger Anmeldung eine Bestätigungsmail senden.

Antwort

200 OK – ein Kontakt-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /contacts/{id}/resubscribe
curl -X POST https://persolz.com/api/v1/contacts/42/resubscribe \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "consent": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/42/resubscribe');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'consent' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/42/resubscribe', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    consent: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/contacts/42/resubscribe",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "consent": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 42,
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "last_name": "Schmidt",
    "address": "Lindenweg 3",
    "zip": "10115",
    "city": "Berlin",
    "phone": "+49 30 1234567",
    "note": "Kam über den Kartenherz-Funnel",
    "tags": ["funnel", "kartenherz"],
    "status": "subscribed",
    "source": "api",
    "custom_fields": {
      "kundennummer": "K-1001"
    },
    "doi_confirmed_at": null,
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00",
    "lists": [
      {
        "id": 3,
        "name": "Kartenherz Funnel",
        "color": "#5B3DF5"
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Aktivitäten eines Kontakts

GET /api/v1/contacts/{id}/activity
Scope read

Zeitstrahl (neueste zuerst): erhaltene, geöffnete und geklickte Mails, Abmeldung, Seifenopern, Termine, Listen, Double-Opt-in – plus Engagement-Kennzahlen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
limit integer optional Max. Einträge (1–200, Standard 80). Beispiel: 80

Antwort

200 OK

Alle 13 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
engagement object Engagement-Kennzahlen des Kontakts.
engagement.sent integer Erhaltene E-Mails.
engagement.opened integer Davon geöffnet.
engagement.clicked integer Davon mit Klick.
engagement.failed integer Nicht zustellbare E-Mails.
engagement.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
engagement.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
engagement.last_activity_at string (Datum/Zeit) Letzte Öffnung oder letzter Klick.
items array<object> Einträge, neueste zuerst.
items[].type string Art: sent, opened, clicked, unsubscribed, soap, appointment, list, doi …
items[].title string Überschrift des Eintrags.
items[].body string Details (z. B. Betreff der Mail).
items[].time string (Datum/Zeit) Zeitpunkt (ISO 8601).

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /contacts/{id}/activity
curl https://persolz.com/api/v1/contacts/42/activity \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/contacts/42/activity');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/contacts/42/activity', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/contacts/42/activity",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "engagement": {
      "sent": 14,
      "opened": 11,
      "clicked": 4,
      "failed": 0,
      "open_rate": 0.7857,
      "click_rate": 0.2857,
      "last_activity_at": "2026-10-09T14:06:12+02:00"
    },
    "items": [
      {
        "type": "opened",
        "title": "E-Mail geöffnet",
        "body": "„Was mein Kaffeebecher über Marketing weiß“",
        "time": "2026-10-09T14:05:00+02:00"
      },
      {
        "type": "list",
        "title": "Zur Liste „Kartenherz Funnel“ hinzugefügt",
        "body": "",
        "time": "2026-10-01T09:00:00+02:00"
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Abmeldungen

Gesperrte Adressen (Abmeldeliste) lesen, ergänzen und aufheben.

Die Abmeldeliste schützt Adressen dauerhaft vor Werbung – auch wenn der Kontakt gelöscht wird. Eine Sperre aufzuheben ist nur mit neuer, nachweisbarer Einwilligung erlaubt (consent=true).

Objekte: Abmelde-Objekt

Das Abmelde-Objekt

Attribute

Das Abmelde-Objekt
FeldBeschreibung
id integer Eindeutige ID.
email string Gesperrte E-Mail-Adresse.
contact_id integer ID des Kontakts oder null (Sperre ohne Kontakt).
source string Quelle: link, one_click, manual, api, import, bounce oder complaint.
source_label string Herkunft in Worten.
reason string Angegebener Grund oder null.
campaign_id integer Kampagne, über die abgemeldet wurde, oder null.
list_id integer · null Liste, aus der abgemeldet wurde, oder null.
created_at string (Datum/Zeit) Zeitpunkt der Abmeldung.
Das Abmelde-Objekt JSON
{
  "id": 77,
  "email": "max@beispiel.de",
  "contact_id": 51,
  "source": "link",
  "source_label": "Abmeldelink",
  "reason": "Zu viele Mails",
  "campaign_id": 812,
  "list_id": null,
  "created_at": "2026-10-08T19:12:00+02:00"
}

Abmeldungen auflisten

GET /api/v1/unsubscribes
Scope read paginiert

Alle gesperrten Adressen, neueste zuerst.

Query-Parameter

NameBeschreibung
email string optional Exakte E-Mail-Adresse. Beispiel: max@beispiel.de
q string optional Suche in E-Mail und Grund. Beispiel: max
source string optional link | one_click | manual | api | import | bounce | complaint Beispiel: link
created_after string optional Abgemeldet nach (ISO 8601 / YYYY-MM-DD). Beispiel: 2026-10-01
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Abmelde-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /unsubscribes
curl "https://persolz.com/api/v1/unsubscribes?created_after=2026-10-01" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'created_after' => '2026-10-01',
]);
$ch = curl_init('https://persolz.com/api/v1/unsubscribes?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  created_after: '2026-10-01'
});
const response = await fetch('https://persolz.com/api/v1/unsubscribes?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/unsubscribes",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "created_after": "2026-10-01",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 77,
      "email": "max@beispiel.de",
      "contact_id": 51,
      "source": "link",
      "source_label": "Abmeldelink",
      "reason": "Zu viele Mails",
      "campaign_id": 812,
      "list_id": null,
      "created_at": "2026-10-08T19:12:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Adresse sperren

POST /api/v1/unsubscribes
Scope write Idempotency-Key

Trägt eine Adresse in die Abmeldeliste ein (auch ohne Kontakt). Ein vorhandener Kontakt wird abgemeldet.

Request-Body application/json

FeldBeschreibung
email string Pflicht E-Mail-Adresse.
reason string optional Grund (optional).

Antwort

201 Gesperrt – ein Abmelde-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 409 conflict 422 validation_error 429 rate_limited

POST /unsubscribes
curl -X POST https://persolz.com/api/v1/unsubscribes \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "max@beispiel.de",
    "reason": "Beschwerde per Mail"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/unsubscribes');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'email' => 'max@beispiel.de',
        'reason' => 'Beschwerde per Mail',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/unsubscribes', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    email: 'max@beispiel.de',
    reason: 'Beschwerde per Mail'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/unsubscribes",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "email": "max@beispiel.de",
        "reason": "Beschwerde per Mail",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "source": "api",
    "source_label": "API",
    "campaign_id": null,
    "id": 77,
    "email": "max@beispiel.de",
    "contact_id": 51,
    "reason": "Zu viele Mails",
    "list_id": null,
    "created_at": "2026-10-08T19:12:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Sperre per E-Mail aufheben

DELETE /api/v1/unsubscribes
Scope write

Wie „Sperre aufheben“, aber über die E-Mail-Adresse statt der ID. Nur mit ?consent=true – Du bestätigst eine neue, nachweisbare Einwilligung.

Query-Parameter

NameBeschreibung
email string optional Gesperrte E-Mail-Adresse (URL-kodiert). Beispiel: max@beispiel.de
consent boolean optional Neue Einwilligung liegt vor (Pflicht). Beispiel: true

Antwort

204 Aufgehoben

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 422 validation_error 429 rate_limited

DELETE /unsubscribes
curl -X DELETE https://persolz.com/api/v1/unsubscribes \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/unsubscribes');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/unsubscribes', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/unsubscribes",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Sperre aufheben

DELETE /api/v1/unsubscribes/{id}
Scope write

Hebt die Abmeldung auf und reaktiviert den Kontakt. Nur mit ?consent=true – Du bestätigst eine neue, nachweisbare Einwilligung.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
consent boolean optional Neue Einwilligung liegt vor (Pflicht). Beispiel: true

Antwort

204 Aufgehoben

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

DELETE /unsubscribes/{id}
curl -X DELETE https://persolz.com/api/v1/unsubscribes/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/unsubscribes/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/unsubscribes/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/unsubscribes/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

API-Referenz

E-Mails

Einzel-E-Mails (z. B. Transaktions- oder Follow-up-Mails) versenden oder planen.

Einzel-E-Mails sind Kampagnen vom Typ email – ideal für Transaktions- und Follow-up-Mails. Newsletter und E-Mails teilen sich das Kampagnen-Objekt.

Objekte: Kampagnen-Objekt

Das Kampagnen-Objekt

Jeder Versand ist eine Kampagne: Einzel-E-Mails, Newsletter, Seinfeld-Mails und Seifenopern-Tage.

Attribute

Das Kampagnen-Objekt
FeldBeschreibung
id integer Eindeutige ID.
type string Versandart: email, newsletter, seinfeld oder soap.
type_label string Typ in Worten (Deutsch).
name string Interner Name.
subject string Betreff.
preheader string Vorschautext (erscheint im Postfach neben dem Betreff).
status string draft, scheduled, queued, sending, sent, paused, cancelled, failed oder active (Seifenopern-Tag).
status_label string Status in Worten (Deutsch).
audience object Empfänger: {all, lists, contacts, emails} oder null.
audience.all boolean An alle aktiven Kontakte senden.
audience.lists array<integer> IDs der Empfänger-Listen.
audience.contacts array IDs einzelner Kontakte.
audience.emails array Einzelne E-Mail-Adressen.
audience_label string Empfänger in Worten, z. B. „1 Liste“.
smtp_account_id integer SMTP-Konto für den Versand oder null (= Standardkonto des Moduls).
attachment_ids array IDs der Anhänge aus der Anhang-Cloud.
include_default_attachments boolean Standard-Anhänge werden zusätzlich mitgesendet.
track_opens boolean Öffnungen werden gemessen.
track_clicks boolean Klicks werden gemessen.
ab_test_id integer · null ID des verknüpften A/B-Tests oder null.
scheduled_at string · null Geplanter Versandzeitpunkt (ISO 8601) oder null.
started_at string (Datum/Zeit) Beginn des Versands oder null.
finished_at string (Datum/Zeit) Ende des Versands oder null.
stats object Kennzahlen des Versands.
stats.recipients integer Empfänger gesamt.
stats.sent integer Versendet.
stats.failed integer Fehlgeschlagen.
stats.unique_opens integer Empfänger mit mindestens einer Öffnung.
stats.unique_clicks integer Empfänger mit mindestens einem Klick.
stats.unsubs integer Abmeldungen.
stats.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
stats.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Kampagnen-Objekt JSON
{
  "id": 812,
  "type": "newsletter",
  "type_label": "Newsletter",
  "name": "Herbst-Aktion",
  "subject": "Anna, das hier ist nur für Dich",
  "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
  "status": "sent",
  "status_label": "Gesendet",
  "audience": {
    "all": false,
    "lists": [3],
    "contacts": [],
    "emails": []
  },
  "audience_label": "1 Liste",
  "smtp_account_id": 1,
  "attachment_ids": [],
  "include_default_attachments": false,
  "track_opens": true,
  "track_clicks": true,
  "ab_test_id": null,
  "scheduled_at": null,
  "started_at": "2026-10-09T09:00:00+02:00",
  "finished_at": "2026-10-09T09:04:31+02:00",
  "stats": {
    "recipients": 1250,
    "sent": 1248,
    "failed": 2,
    "unique_opens": 702,
    "unique_clicks": 133,
    "unsubs": 3,
    "open_rate": 0.5625,
    "click_rate": 0.1066
  },
  "created_at": "2026-10-08T17:20:00+02:00",
  "updated_at": "2026-10-09T09:04:31+02:00"
}

E-Mail senden oder planen

POST /api/v1/emails/send
Scope send Tarif: email Idempotency-Key

Versendet eine E-Mail an einzelne Adressen, Kontakte oder Listen – sofort oder geplant (scheduled_at). Inhalt als html, text oder aus einer Vorlage (template_key aus der Bibliothek bzw. template_id einer eigenen Vorlage). Platzhalter wie {{vorname}} werden je Empfänger ersetzt, „placeholders“ ersetzt zusätzlich eigene Platzhalter für alle Empfänger. Abgemeldete und unbestätigte Kontakte werden automatisch ausgeschlossen. Antwort 202: die Mail ist in der Warteschlange (Kampagnen-ID zum Nachverfolgen).

Request-Body application/json

FeldBeschreibung
to string|array Pflicht Empfänger: eine E-Mail, kommagetrennte E-Mails, ein Array aus E-Mails und/oder Kontakt-IDs.
list_ids array<integer> optional Zusätzlich alle aktiven Mitglieder dieser Listen.
subject string Pflicht Betreff (Pflicht, außer die Vorlage liefert einen). Platzhalter erlaubt.
html string optional HTML-Inhalt (max. 2 MB).
text string optional Alternativ: reiner Text (wird in eine schlichte HTML-Mail verpackt).
template_key string optional Bibliotheks-Vorlage, z. B. email-07.
template_id integer optional Eigene Vorlage.
preheader string optional Vorschautext.
placeholders object optional Eigene Platzhalter für alle Empfänger, z. B. {"bestellnummer":"4711"} → {{bestellnummer}}.
smtp_account_id integer optional SMTP-Konto (Standard: Modul- bzw. Standardkonto).
attachment_ids array<integer> optional Anhänge aus der Anhang-Cloud.
include_default_attachments boolean optional Standard-Anhänge mitsenden (Standard false).
scheduled_at string optional Geplanter Versand (ISO 8601, max. 1 Jahr). Leer = sofort.
name string optional Interner Name (Standard: Betreff).
from_name string optional Abweichender Absendername.
reply_to string optional Abweichende Antwort-Adresse.
track_opens boolean optional Öffnungen messen (Standard true).
track_clicks boolean optional Klicks messen (Standard true).

Antwort

202 In der Warteschlange bzw. geplant

Antwortfelder
FeldBeschreibung
campaign_id integer ID der E-Mail – damit fragst Du den Status ab (GET /emails/{id}).
status string sending (wird versendet) oder scheduled (geplant).
scheduled_at string · null Geplanter Versandzeitpunkt (ISO 8601) oder null.
recipients integer Empfänger gesamt.
queued integer In die Warteschlange gestellte E-Mails.
sent integer Bereits versendet (Rest folgt automatisch).
failed integer Fehlgeschlagen.
message string Verständliche Meldung auf Deutsch.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited

POST /emails/send
curl -X POST https://persolz.com/api/v1/emails/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "to": ["anna@beispiel.de"],
    "subject": "Deine Bestellung #{{bestellnummer}}",
    "html": "<p>Hallo {{vorname|Freund}},</p><p>danke für Deine Bestellung {{bestellnummer}}!</p>",
    "placeholders": {
      "bestellnummer": "4711"
    },
    "attachment_ids": [9]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/emails/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => ['anna@beispiel.de'],
        'subject' => 'Deine Bestellung #{{bestellnummer}}',
        'html' => '<p>Hallo {{vorname|Freund}},</p><p>danke für Deine Bestellung {{bestellnummer}}!</p>',
        'placeholders' => [
            'bestellnummer' => '4711',
        ],
        'attachment_ids' => [9],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/emails/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    to: ['anna@beispiel.de'],
    subject: 'Deine Bestellung #{{bestellnummer}}',
    html: '<p>Hallo {{vorname|Freund}},</p><p>danke für Deine Bestellung {{bestellnummer}}!</p>',
    placeholders: {
      bestellnummer: '4711'
    },
    attachment_ids: [9]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/emails/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "to": ["anna@beispiel.de"],
        "subject": "Deine Bestellung #{{bestellnummer}}",
        "html": "<p>Hallo {{vorname|Freund}},</p><p>danke für Deine Bestellung {{bestellnummer}}!</p>",
        "placeholders": {
            "bestellnummer": "4711",
        },
        "attachment_ids": [9],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 202 JSON
{
  "data": {
    "campaign_id": 813,
    "status": "sending",
    "scheduled_at": null,
    "recipients": 1,
    "queued": 1,
    "sent": 1,
    "failed": 0,
    "message": "Versand abgeschlossen: 1 E-Mail verschickt."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

E-Mails auflisten

GET /api/v1/emails
Scope read Tarif: email paginiert

Einzel-E-Mails (Kampagnen vom Typ email).

Query-Parameter

NameBeschreibung
status string optional all | sent | scheduled | drafts Werte: all, sent, scheduled, drafts Beispiel: all
q string optional Suche in Betreff/Name. Beispiel: bestellung
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Kampagnen-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /emails
curl "https://persolz.com/api/v1/emails?status=sent" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'sent',
]);
$ch = curl_init('https://persolz.com/api/v1/emails?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'sent'
});
const response = await fetch('https://persolz.com/api/v1/emails?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/emails",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "sent",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 812,
      "type": "email",
      "type_label": "E-Mail",
      "name": "Herbst-Aktion",
      "subject": "Anna, das hier ist nur für Dich",
      "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
      "status": "sent",
      "status_label": "Gesendet",
      "audience": {
        "all": false,
        "lists": [3],
        "contacts": [],
        "emails": []
      },
      "audience_label": "1 Liste",
      "smtp_account_id": 1,
      "attachment_ids": [],
      "include_default_attachments": false,
      "track_opens": true,
      "track_clicks": true,
      "ab_test_id": null,
      "scheduled_at": null,
      "started_at": "2026-10-09T09:00:00+02:00",
      "finished_at": "2026-10-09T09:04:31+02:00",
      "stats": {
        "recipients": 1250,
        "sent": 1248,
        "failed": 2,
        "unique_opens": 702,
        "unique_clicks": 133,
        "unsubs": 3,
        "open_rate": 0.5625,
        "click_rate": 0.1066
      },
      "created_at": "2026-10-08T17:20:00+02:00",
      "updated_at": "2026-10-09T09:04:31+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

E-Mail abrufen

GET /api/v1/emails/{id}
Scope read Tarif: email

Eine E-Mail inklusive HTML und Versandstatistik.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Kampagnen-Objekt, zusätzlich:

Antwortfelder
FeldBeschreibung
html string HTML-Inhalt mit Platzhaltern.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /emails/{id}
curl https://persolz.com/api/v1/emails/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/emails/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/emails/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/emails/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "type": "email",
    "type_label": "E-Mail",
    "name": "Herbst-Aktion",
    "subject": "Anna, das hier ist nur für Dich",
    "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
    "status": "sent",
    "status_label": "Gesendet",
    "audience": {
      "all": false,
      "lists": [3],
      "contacts": [],
      "emails": []
    },
    "audience_label": "1 Liste",
    "smtp_account_id": 1,
    "attachment_ids": [],
    "include_default_attachments": false,
    "track_opens": true,
    "track_clicks": true,
    "ab_test_id": null,
    "scheduled_at": null,
    "started_at": "2026-10-09T09:00:00+02:00",
    "finished_at": "2026-10-09T09:04:31+02:00",
    "stats": {
      "recipients": 1250,
      "sent": 1248,
      "failed": 2,
      "unique_opens": 702,
      "unique_clicks": 133,
      "unsubs": 3,
      "open_rate": 0.5625,
      "click_rate": 0.1066
    },
    "created_at": "2026-10-08T17:20:00+02:00",
    "updated_at": "2026-10-09T09:04:31+02:00",
    "html": "<p>Hallo {{vorname}} …</p>"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Newsletter

Newsletter als Entwurf anlegen, testen, versenden, planen und auswerten.

Ablauf: Entwurf anlegen → Testmail → senden oder planen. Newsletter verwenden das Kampagnen-Objekt.

Newsletter auflisten

GET /api/v1/newsletters
Scope read Tarif: newsletter paginiert

Alle Newsletter mit Status und Kennzahlen.

Query-Parameter

NameBeschreibung
status string optional all | drafts | scheduled | sent Werte: all, drafts, scheduled, sent Beispiel: all
q string optional Suche in Betreff/Name. Beispiel: herbst
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Kampagnen-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /newsletters
curl "https://persolz.com/api/v1/newsletters?status=sent" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'sent',
]);
$ch = curl_init('https://persolz.com/api/v1/newsletters?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'sent'
});
const response = await fetch('https://persolz.com/api/v1/newsletters?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/newsletters",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "sent",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 812,
      "type": "newsletter",
      "type_label": "Newsletter",
      "name": "Herbst-Aktion",
      "subject": "Anna, das hier ist nur für Dich",
      "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
      "status": "sent",
      "status_label": "Gesendet",
      "audience": {
        "all": false,
        "lists": [3],
        "contacts": [],
        "emails": []
      },
      "audience_label": "1 Liste",
      "smtp_account_id": 1,
      "attachment_ids": [],
      "include_default_attachments": false,
      "track_opens": true,
      "track_clicks": true,
      "ab_test_id": null,
      "scheduled_at": null,
      "started_at": "2026-10-09T09:00:00+02:00",
      "finished_at": "2026-10-09T09:04:31+02:00",
      "stats": {
        "recipients": 1250,
        "sent": 1248,
        "failed": 2,
        "unique_opens": 702,
        "unique_clicks": 133,
        "unsubs": 3,
        "open_rate": 0.5625,
        "click_rate": 0.1066
      },
      "created_at": "2026-10-08T17:20:00+02:00",
      "updated_at": "2026-10-09T09:04:31+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Newsletter-Entwurf anlegen

POST /api/v1/newsletters
Scope write Tarif: newsletter Idempotency-Key

Legt einen Newsletter als Entwurf an (es wird nichts versendet). Vollständigkeit (Betreff, Inhalt, Empfänger, SMTP) wird erst beim Versand geprüft.

Request-Body application/json

FeldBeschreibung
name string optional Interner Name.
subject string optional Betreff (zum Versand Pflicht).
preheader string optional Vorschautext.
html string optional HTML-Inhalt.
text string optional Alternativ reiner Text.
template_key string optional Bibliotheks-Vorlage (übernimmt HTML, Betreff, Preheader, falls nicht angegeben).
template_id integer optional Eigene Vorlage.
audience object optional Empfänger: {"all":false,"lists":[3],"contacts":[42],"emails":["x@y.de"]}.
list_ids array<integer> optional Kurzform für audience.lists.
smtp_account_id integer optional SMTP-Konto.
attachment_ids array<integer> optional Anhänge.
include_default_attachments boolean optional Standard-Anhänge mitsenden.
from_name string optional Abweichender Absendername.
reply_to string optional Abweichende Antwort-Adresse.
track_opens boolean optional Öffnungen messen.
track_clicks boolean optional Klicks messen.

Antwort

201 Entwurf angelegt – ein Kampagnen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 422 validation_error 429 rate_limited

POST /newsletters
curl -X POST https://persolz.com/api/v1/newsletters \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Herbst-Aktion",
    "subject": "Anna, das hier ist nur für Dich",
    "template_key": "newsletter-01",
    "list_ids": [3]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'name' => 'Herbst-Aktion',
        'subject' => 'Anna, das hier ist nur für Dich',
        'template_key' => 'newsletter-01',
        'list_ids' => [3],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Herbst-Aktion',
    subject: 'Anna, das hier ist nur für Dich',
    template_key: 'newsletter-01',
    list_ids: [3]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/newsletters",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "name": "Herbst-Aktion",
        "subject": "Anna, das hier ist nur für Dich",
        "template_key": "newsletter-01",
        "list_ids": [3],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "status": "draft",
    "status_label": "Entwurf",
    "started_at": null,
    "finished_at": null,
    "id": 812,
    "type": "newsletter",
    "type_label": "Newsletter",
    "name": "Herbst-Aktion",
    "subject": "Anna, das hier ist nur für Dich",
    "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
    "audience": {
      "all": false,
      "lists": [3],
      "contacts": [],
      "emails": []
    },
    "audience_label": "1 Liste",
    "smtp_account_id": 1,
    "attachment_ids": [],
    "include_default_attachments": false,
    "track_opens": true,
    "track_clicks": true,
    "ab_test_id": null,
    "scheduled_at": null,
    "stats": {
      "recipients": 1250,
      "sent": 1248,
      "failed": 2,
      "unique_opens": 702,
      "unique_clicks": 133,
      "unsubs": 3,
      "open_rate": 0.5625,
      "click_rate": 0.1066
    },
    "created_at": "2026-10-08T17:20:00+02:00",
    "updated_at": "2026-10-09T09:04:31+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Newsletter abrufen

GET /api/v1/newsletters/{id}
Scope read Tarif: newsletter

Ein Newsletter inklusive HTML und Kennzahlen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Kampagnen-Objekt, zusätzlich:

Antwortfelder
FeldBeschreibung
html string HTML-Inhalt mit Platzhaltern.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /newsletters/{id}
curl https://persolz.com/api/v1/newsletters/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/newsletters/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "type": "newsletter",
    "type_label": "Newsletter",
    "name": "Herbst-Aktion",
    "subject": "Anna, das hier ist nur für Dich",
    "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
    "status": "sent",
    "status_label": "Gesendet",
    "audience": {
      "all": false,
      "lists": [3],
      "contacts": [],
      "emails": []
    },
    "audience_label": "1 Liste",
    "smtp_account_id": 1,
    "attachment_ids": [],
    "include_default_attachments": false,
    "track_opens": true,
    "track_clicks": true,
    "ab_test_id": null,
    "scheduled_at": null,
    "started_at": "2026-10-09T09:00:00+02:00",
    "finished_at": "2026-10-09T09:04:31+02:00",
    "stats": {
      "recipients": 1250,
      "sent": 1248,
      "failed": 2,
      "unique_opens": 702,
      "unique_clicks": 133,
      "unsubs": 3,
      "open_rate": 0.5625,
      "click_rate": 0.1066
    },
    "created_at": "2026-10-08T17:20:00+02:00",
    "updated_at": "2026-10-09T09:04:31+02:00",
    "html": "<!doctype html><html>…</html>"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Newsletter ändern

PATCH /api/v1/newsletters/{id}
Scope write Tarif: newsletter

Ändert einen Entwurf oder geplanten Newsletter (versendete sind gesperrt → 409).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
name string optional Interner Name.
subject string optional Betreff (zum Versand Pflicht).
preheader string optional Vorschautext.
html string optional HTML-Inhalt.
text string optional Alternativ reiner Text.
template_key string optional Bibliotheks-Vorlage (übernimmt HTML, Betreff, Preheader, falls nicht angegeben).
template_id integer optional Eigene Vorlage.
audience object optional Empfänger: {"all":false,"lists":[3],"contacts":[42],"emails":["x@y.de"]}.
list_ids array<integer> optional Kurzform für audience.lists.
smtp_account_id integer optional SMTP-Konto.
attachment_ids array<integer> optional Anhänge.
include_default_attachments boolean optional Standard-Anhänge mitsenden.
from_name string optional Abweichender Absendername.
reply_to string optional Abweichende Antwort-Adresse.
track_opens boolean optional Öffnungen messen.
track_clicks boolean optional Klicks messen.

Antwort

200 OK – ein Kampagnen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

PATCH /newsletters/{id}
curl -X PATCH https://persolz.com/api/v1/newsletters/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Neuer Betreff",
    "list_ids": [3, 4]
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'subject' => 'Neuer Betreff',
        'list_ids' => [3, 4],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    subject: 'Neuer Betreff',
    list_ids: [3, 4]
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/newsletters/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "subject": "Neuer Betreff",
        "list_ids": [3, 4],
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "type": "newsletter",
    "type_label": "Newsletter",
    "name": "Herbst-Aktion",
    "subject": "Anna, das hier ist nur für Dich",
    "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
    "status": "sent",
    "status_label": "Gesendet",
    "audience": {
      "all": false,
      "lists": [3],
      "contacts": [],
      "emails": []
    },
    "audience_label": "1 Liste",
    "smtp_account_id": 1,
    "attachment_ids": [],
    "include_default_attachments": false,
    "track_opens": true,
    "track_clicks": true,
    "ab_test_id": null,
    "scheduled_at": null,
    "started_at": "2026-10-09T09:00:00+02:00",
    "finished_at": "2026-10-09T09:04:31+02:00",
    "stats": {
      "recipients": 1250,
      "sent": 1248,
      "failed": 2,
      "unique_opens": 702,
      "unique_clicks": 133,
      "unsubs": 3,
      "open_rate": 0.5625,
      "click_rate": 0.1066
    },
    "created_at": "2026-10-08T17:20:00+02:00",
    "updated_at": "2026-10-09T09:04:31+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Newsletter löschen

DELETE /api/v1/newsletters/{id}
Scope write Tarif: newsletter

Löscht den Newsletter samt Statistik. Laufende Versände müssen zuerst abgebrochen werden (409).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 429 rate_limited

DELETE /newsletters/{id}
curl -X DELETE https://persolz.com/api/v1/newsletters/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/newsletters/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/newsletters/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Newsletter jetzt senden

POST /api/v1/newsletters/{id}/send
Scope send Tarif: newsletter Idempotency-Key

Prüft Vollständigkeit und Tageslimit, stellt alle Empfänger in die Warteschlange und verschickt einen ersten Schwung. Der Rest folgt über den Cron.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

202 Versand gestartet

Antwortfelder
FeldBeschreibung
id integer ID des Newsletters.
status string Neuer Status: sending (Versand läuft), scheduled (geplant) oder draft (Entwurf).
queued integer In die Warteschlange gestellte E-Mails.
sent integer Bereits versendet (der Rest folgt automatisch über den Cron).
failed integer Fehlgeschlagen.
message string Verständliche Meldung auf Deutsch.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

POST /newsletters/{id}/send
curl -X POST https://persolz.com/api/v1/newsletters/42/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/42/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Idempotency-Key': crypto.randomUUID()
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/newsletters/42/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 202 JSON
{
  "data": {
    "id": 812,
    "status": "sending",
    "queued": 1250,
    "sent": 150,
    "failed": 0,
    "message": "Versand gestartet: 150 von 1.250 E-Mails sind raus, der Rest folgt automatisch."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Newsletter planen

POST /api/v1/newsletters/{id}/schedule
Scope send Tarif: newsletter Idempotency-Key

Plant den Versand für einen Zeitpunkt in der Zukunft (max. 1 Jahr).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
scheduled_at string Pflicht Zeitpunkt (ISO 8601).

Antwort

200 OK

Antwortfelder
FeldBeschreibung
id integer ID des Newsletters.
status string Neuer Status: sending (Versand läuft), scheduled (geplant) oder draft (Entwurf).
scheduled_at string (Datum/Zeit) Geplanter Versandzeitpunkt (ISO 8601) oder null.
message string Verständliche Meldung auf Deutsch.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

POST /newsletters/{id}/schedule
curl -X POST https://persolz.com/api/v1/newsletters/42/schedule \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduled_at": "2026-10-12T09:00:00+02:00"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42/schedule');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'scheduled_at' => '2026-10-12T09:00:00+02:00',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/42/schedule', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    scheduled_at: '2026-10-12T09:00:00+02:00'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/newsletters/42/schedule",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "scheduled_at": "2026-10-12T09:00:00+02:00",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "status": "scheduled",
    "scheduled_at": "2026-10-12T09:00:00+02:00",
    "message": "Newsletter geplant für 12.10.2026 09:00 Uhr – wir senden pünktlich."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Versand abbrechen

POST /api/v1/newsletters/{id}/cancel
Scope send Tarif: newsletter Idempotency-Key

Geplante Newsletter werden wieder zum Entwurf, laufende Versände werden gestoppt.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK

Antwortfelder
FeldBeschreibung
id integer ID des Newsletters.
status string Neuer Status: sending (Versand läuft), scheduled (geplant) oder draft (Entwurf).
message string Verständliche Meldung auf Deutsch.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 429 rate_limited

POST /newsletters/{id}/cancel
curl -X POST https://persolz.com/api/v1/newsletters/42/cancel \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42/cancel');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/42/cancel', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Idempotency-Key': crypto.randomUUID()
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/newsletters/42/cancel",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "status": "draft",
    "message": "Versand abgebrochen – der Newsletter liegt wieder bei Deinen Entwürfen."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Testmail senden

POST /api/v1/newsletters/{id}/test
Scope send Tarif: newsletter Idempotency-Key

Sendet den aktuellen Stand als Testmail (Betreff mit „[TEST]“, ohne Tracking) an bis zu 5 Adressen. Ohne „to“ an Deine Konto-Adresse.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
to string|array optional Bis zu 5 Adressen.
contact_id integer optional Kontakt für die Personalisierung (Standard: Beispielkontakt „Max“).

Antwort

200 OK

Antwortfelder
FeldBeschreibung
sent_to array<string> Empfänger der Testmail.
message string Verständliche Meldung auf Deutsch.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /newsletters/{id}/test
curl -X POST https://persolz.com/api/v1/newsletters/42/test \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "du@beispiel.de"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42/test');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'to' => 'du@beispiel.de',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/42/test', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    to: 'du@beispiel.de'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/newsletters/42/test",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "to": "du@beispiel.de",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "sent_to": ["du@beispiel.de"],
    "message": "Testmail an du@beispiel.de gesendet."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Newsletter-Statistik

GET /api/v1/newsletters/{id}/stats
Scope read Tarif: newsletter

Zahlen und Quoten (Öffnungen, Klicks, Abmeldungen, Bounces), Top-Links und Geräte.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK

Alle 31 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
id integer ID des Newsletters.
status string Status des Newsletters.
stats object Kennzahlen.
stats.recipients integer Empfänger gesamt.
stats.sent integer Versendet.
stats.failed integer Fehlgeschlagen.
stats.pending integer Noch nicht versendet.
stats.held integer Wartet auf den Gewinner eines A/B-Tests.
stats.skipped integer Übersprungen (z. B. inzwischen abgemeldet).
stats.cancelled integer Abgebrochen.
stats.opens integer Öffnungen gesamt (inkl. mehrfacher Öffnungen).
stats.unique_opens integer Empfänger mit mindestens einer Öffnung.
stats.clicks integer Klicks gesamt.
stats.unique_clicks integer Empfänger mit mindestens einem Klick.
stats.unsubs integer Abmeldungen.
stats.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
stats.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
stats.ctor number Click-to-Open-Rate von 0 bis 1 (eindeutige Klicks / eindeutige Öffnungen).
stats.unsub_rate number Abmeldequote von 0 bis 1.
stats.bounce_rate number Bounce-Quote von 0 bis 1.
stats.failed_rate number Anteil fehlgeschlagener Zustellungen von 0 bis 1.
stats.progress number Fortschritt des Versands von 0 bis 1.
links array<object> Meistgeklickte Links.
links[].url string Ziel-URL des Links.
links[].label string Linktext.
links[].clicks integer Klicks gesamt.
links[].unique_clicks integer Empfänger, die geklickt haben.
devices object Öffnungen nach Gerät.
devices.desktop integer Öffnungen am Desktop.
devices.mobile integer Öffnungen auf dem Smartphone.
devices.tablet integer Öffnungen auf dem Tablet.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /newsletters/{id}/stats
curl https://persolz.com/api/v1/newsletters/42/stats \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/newsletters/42/stats');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/newsletters/42/stats', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/newsletters/42/stats",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "status": "sent",
    "stats": {
      "recipients": 1250,
      "sent": 1248,
      "failed": 2,
      "pending": 0,
      "held": 0,
      "skipped": 0,
      "cancelled": 0,
      "opens": 980,
      "unique_opens": 702,
      "clicks": 190,
      "unique_clicks": 133,
      "unsubs": 3,
      "open_rate": 0.5625,
      "click_rate": 0.1066,
      "ctor": 0.1895,
      "unsub_rate": 0.0024,
      "bounce_rate": 0.0016,
      "failed_rate": 0.0016,
      "progress": 1.0
    },
    "links": [
      {
        "url": "https://kartenherz.com/angebot",
        "label": "Jetzt ansehen",
        "clicks": 140,
        "unique_clicks": 118
      }
    ],
    "devices": {
      "desktop": 312,
      "mobile": 365,
      "tablet": 25
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Das Seinfeld-Einstellungs-Objekt

Attribute

Das Seinfeld-Einstellungs-Objekt
FeldBeschreibung
enabled boolean Seinfeld-Mails sind aktiv.
frequency string Rhythmus: daily (täglich), weekdays (Mo–Fr), custom (eigene Wochentage) oder interval (alle n Tage).
weekdays array<integer> Versandtage bei custom: 1 (Mo) bis 7 (So).
every_n_days integer Abstand in Tagen bei interval.
anchor string (Datum) Startdatum des Rhythmus bei interval.
send_time string Versandzeit (HH:MM, Europe/Berlin).
smtp_account_id integer · null SMTP-Konto für den Versand oder null (= Standardkonto des Moduls).
audience object Empfänger: {all, lists, contacts, emails} oder null.
audience.all boolean An alle aktiven Kontakte senden.
audience.lists array<integer> IDs der Empfänger-Listen.
audience.contacts array IDs einzelner Kontakte.
audience.emails array Einzelne E-Mail-Adressen.
auto_generate boolean Die KI schreibt automatisch Entwürfe für freie Termine.
auto_send boolean KI-Entwürfe werden ohne Freigabe automatisch geplant.
plan_ahead_days integer So viele Tage im Voraus werden Entwürfe vorbereitet.
ai_model string KI-Modell: sonnet (beste Qualität) oder haiku (schnell & günstig).
persona object Persona, mit der die KI schreibt.
persona.name string Name des Absenders in den Geschichten.
persona.character string Charakter.
persona.background string Hintergrund.
persona.style string Schreibstil.
audience_desc string Beschreibung Deiner Zielgruppe (für die KI).
offer_name string Name Deines Angebots.
offer_url string Link zum Angebot.
offer_desc string Kurzbeschreibung des Angebots.
tone string Tonalität.
signature string Grußformel unter jeder Mail.
template_key string Design-Vorlage aus der Vorlagen-Bibliothek (z. B. seinfeld-01) oder null.
continuity_mode string Story-Kontinuität: z. B. arcs (zusammenhängende Story-Bögen).
include_default_attachments boolean Standard-Anhänge werden zusätzlich mitgesendet.
rhythm string Rhythmus in Worten.
streak integer Aktuelle Serie: Versandtage in Folge ohne Lücke.
Das Seinfeld-Einstellungs-Objekt JSON
{
  "enabled": true,
  "frequency": "weekdays",
  "weekdays": [1, 2, 3, 4, 5],
  "every_n_days": 2,
  "anchor": "2026-10-01",
  "send_time": "09:00",
  "smtp_account_id": null,
  "audience": {
    "all": false,
    "lists": [3],
    "contacts": [],
    "emails": []
  },
  "auto_generate": true,
  "auto_send": false,
  "plan_ahead_days": 3,
  "ai_model": "sonnet",
  "persona": {
    "name": "Daniel",
    "character": "Humorvoller Kartenmacher",
    "background": "Seit 10 Jahren Illustrator",
    "style": "locker, Du-Form"
  },
  "audience_desc": "Menschen, die gern Karten verschicken",
  "offer_name": "Kartenset-Abo",
  "offer_url": "https://kartenherz.com/abo",
  "offer_desc": "Jeden Monat 5 handgezeichnete Karten",
  "tone": "unterhaltsam",
  "signature": "Dein Daniel",
  "template_key": "seinfeld-01",
  "continuity_mode": "arcs",
  "include_default_attachments": false,
  "rhythm": "Montag bis Freitag um 09:00 Uhr",
  "streak": 12
}

Das Themen-Objekt

Attribute

Das Themen-Objekt
FeldBeschreibung
id integer Eindeutige ID.
topic string Das Thema (Stichwort oder Satz).
notes string Notizen für die KI oder für Dich.
priority boolean Thema mit Stern – wird zuerst verwendet.
status string open (offen), used (verwendet) oder skipped (übersprungen).
used_at string · null Zeitpunkt, an dem das Thema verwendet wurde.
email_id integer · null Seinfeld-Mail, die das Thema verwendet, oder null.
sort_order integer Reihenfolge.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
Das Themen-Objekt JSON
{
  "id": 15,
  "topic": "Der verlorene Schlüssel",
  "notes": "Bezug zu Ordnungssystemen",
  "priority": true,
  "status": "open",
  "used_at": null,
  "email_id": null,
  "sort_order": 3,
  "created_at": "2026-10-01T09:00:00+02:00"
}

Das Seinfeld-Mail-Objekt

Eine Seinfeld-Mail mit Story-Gedächtnis (Titel, Zusammenfassung, Stichworte).

Attribute

Das Seinfeld-Mail-Objekt
FeldBeschreibung
id integer Eindeutige ID.
subject string Betreff.
preheader string Vorschautext (erscheint im Postfach neben dem Betreff).
html string HTML-Inhalt.
status string draft (Entwurf), pending (KI-Entwurf zur Freigabe), scheduled (geplant), sending, sent, failed oder cancelled.
status_label string Status in Worten (Deutsch).
ai_generated boolean Von der KI geschrieben.
ai_model string Verwendetes KI-Modell oder null.
topic_id integer Verwendetes Thema oder null.
arc_id integer · null Story-Bogen oder null.
episode_no string · null Folge im Story-Bogen oder null.
story_title string Titel der Geschichte (Story-Gedächtnis gegen Wiederholungen).
story_summary string Kurze Zusammenfassung (Story-Gedächtnis).
keywords array<string> Stichworte.
template_key string Design-Vorlage aus der Vorlagen-Bibliothek (z. B. seinfeld-01) oder null.
scheduled_at string (Datum/Zeit) Geplanter Versandzeitpunkt (ISO 8601) oder null.
sent_at string · null Zeitpunkt des Versands oder null.
campaign_id integer Kampagne des Versands (nach dem Planen) oder null.
smtp_account_id integer · null SMTP-Konto für den Versand oder null (= Standardkonto des Moduls).
audience object · null Abweichende Empfänger oder null (= Einstellungen).
attachment_ids array IDs der Anhänge aus der Anhang-Cloud.
include_default_attachments boolean Standard-Anhänge werden zusätzlich mitgesendet.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Seinfeld-Mail-Objekt JSON
{
  "id": 120,
  "subject": "Was mein Kaffeebecher über Marketing weiß",
  "preheader": "Eine Geschichte über Gewohnheiten",
  "html": "<!doctype html><html>…</html>",
  "status": "scheduled",
  "status_label": "Geplant",
  "ai_generated": true,
  "ai_model": "claude-sonnet-5-5",
  "topic_id": 15,
  "arc_id": null,
  "episode_no": null,
  "story_title": "Der Kaffeebecher",
  "story_summary": "Warum kleine Rituale Vertrauen schaffen.",
  "keywords": ["kaffee", "gewohnheit"],
  "template_key": "seinfeld-01",
  "scheduled_at": "2026-10-10T09:00:00+02:00",
  "sent_at": null,
  "campaign_id": 901,
  "smtp_account_id": null,
  "audience": null,
  "attachment_ids": [],
  "include_default_attachments": false,
  "created_at": "2026-10-09T07:00:00+02:00",
  "updated_at": "2026-10-09T08:12:00+02:00"
}

Seinfeld-Einstellungen

GET /api/v1/seinfeld/settings
Scope read Tarif: seinfeld

Rhythmus, Versandzeit, Empfänger, KI-Persona, Angebot und Automatik-Optionen – plus aktuelle Serie (Streak).

Antwort

200 OK – ein Seinfeld-Einstellungs-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /seinfeld/settings
curl https://persolz.com/api/v1/seinfeld/settings \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/settings');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/settings', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/seinfeld/settings",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "enabled": true,
    "frequency": "weekdays",
    "weekdays": [1, 2, 3, 4, 5],
    "every_n_days": 2,
    "anchor": "2026-10-01",
    "send_time": "09:00",
    "smtp_account_id": null,
    "audience": {
      "all": false,
      "lists": [3],
      "contacts": [],
      "emails": []
    },
    "auto_generate": true,
    "auto_send": false,
    "plan_ahead_days": 3,
    "ai_model": "sonnet",
    "persona": {
      "name": "Daniel",
      "character": "Humorvoller Kartenmacher",
      "background": "Seit 10 Jahren Illustrator",
      "style": "locker, Du-Form"
    },
    "audience_desc": "Menschen, die gern Karten verschicken",
    "offer_name": "Kartenset-Abo",
    "offer_url": "https://kartenherz.com/abo",
    "offer_desc": "Jeden Monat 5 handgezeichnete Karten",
    "tone": "unterhaltsam",
    "signature": "Dein Daniel",
    "template_key": "seinfeld-01",
    "continuity_mode": "arcs",
    "include_default_attachments": false,
    "rhythm": "Montag bis Freitag um 09:00 Uhr",
    "streak": 12
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seinfeld-Einstellungen speichern

PUT /api/v1/seinfeld/settings
Scope write Tarif: seinfeld

Übernimmt die übergebenen Felder, alle anderen behalten ihren Wert.

Request-Body application/json

FeldBeschreibung
enabled boolean optional Seinfeld-Mails aktiv.
frequency string optional daily | weekdays | custom | interval Werte: daily, weekdays, custom, interval
weekdays array<integer> optional Bei custom: Wochentage 1 (Mo) – 7 (So).
every_n_days integer optional Bei interval: Abstand in Tagen (2–30).
send_time string optional Uhrzeit HH:MM.
audience object optional Empfänger {all, lists, contacts, emails}.
auto_generate boolean optional KI schreibt Entwürfe für freie Termine.
auto_send boolean optional KI-Entwürfe ohne Freigabe automatisch planen.
plan_ahead_days integer optional So viele Tage im Voraus vorbereiten (1–14).
ai_model string optional sonnet | haiku
persona object optional {name, character, background, style}.
offer_name string optional Angebot.
offer_url string optional Link zum Angebot.
tone string optional Tonalität.
signature string optional Grußformel.
template_key string optional Seinfeld-Vorlage.
smtp_account_id integer optional SMTP-Konto.

Antwort

200 OK – ein Seinfeld-Einstellungs-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 422 validation_error 429 rate_limited

PUT /seinfeld/settings
curl -X PUT https://persolz.com/api/v1/seinfeld/settings \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "frequency": "weekdays",
    "send_time": "09:00",
    "audience": {
      "lists": [3]
    },
    "offer_url": "https://kartenherz.com/abo"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/settings');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'enabled' => true,
        'frequency' => 'weekdays',
        'send_time' => '09:00',
        'audience' => [
            'lists' => [3],
        ],
        'offer_url' => 'https://kartenherz.com/abo',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/settings', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    enabled: true,
    frequency: 'weekdays',
    send_time: '09:00',
    audience: {
      lists: [3]
    },
    offer_url: 'https://kartenherz.com/abo'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.put(
    "https://persolz.com/api/v1/seinfeld/settings",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "enabled": True,
        "frequency": "weekdays",
        "send_time": "09:00",
        "audience": {
            "lists": [3],
        },
        "offer_url": "https://kartenherz.com/abo",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "enabled": true,
    "frequency": "weekdays",
    "weekdays": [1, 2, 3, 4, 5],
    "every_n_days": 2,
    "anchor": "2026-10-01",
    "send_time": "09:00",
    "smtp_account_id": null,
    "audience": {
      "all": false,
      "lists": [3],
      "contacts": [],
      "emails": []
    },
    "auto_generate": true,
    "auto_send": false,
    "plan_ahead_days": 3,
    "ai_model": "sonnet",
    "persona": {
      "name": "Daniel",
      "character": "Humorvoller Kartenmacher",
      "background": "Seit 10 Jahren Illustrator",
      "style": "locker, Du-Form"
    },
    "audience_desc": "Menschen, die gern Karten verschicken",
    "offer_name": "Kartenset-Abo",
    "offer_url": "https://kartenherz.com/abo",
    "offer_desc": "Jeden Monat 5 handgezeichnete Karten",
    "tone": "unterhaltsam",
    "signature": "Dein Daniel",
    "template_key": "seinfeld-01",
    "continuity_mode": "arcs",
    "include_default_attachments": false,
    "rhythm": "Montag bis Freitag um 09:00 Uhr",
    "streak": 12
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Themen auflisten

GET /api/v1/seinfeld/topics
Scope read Tarif: seinfeld paginiert

Die Themenliste (offen zuerst in manueller Reihenfolge).

Query-Parameter

NameBeschreibung
status string optional all | open | used | skipped Werte: all, open, used, skipped Beispiel: open
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Themen-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /seinfeld/topics
curl "https://persolz.com/api/v1/seinfeld/topics?status=open" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'open',
]);
$ch = curl_init('https://persolz.com/api/v1/seinfeld/topics?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'open'
});
const response = await fetch('https://persolz.com/api/v1/seinfeld/topics?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/seinfeld/topics",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "open",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 15,
      "topic": "Der verlorene Schlüssel",
      "notes": "Bezug zu Ordnungssystemen",
      "priority": true,
      "status": "open",
      "used_at": null,
      "email_id": null,
      "sort_order": 3,
      "created_at": "2026-10-01T09:00:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Thema hinzufügen

POST /api/v1/seinfeld/topics
Scope write Tarif: seinfeld Idempotency-Key

Fügt ein Thema hinzu – oder mehrere mit „topics“ (Array aus Texten oder {topic, note}). Doppelte Themen werden übersprungen.

Request-Body application/json

FeldBeschreibung
topic string optional Thema.
notes string optional Notizen.
priority boolean optional Mit Stern (wird zuerst genutzt).
topics array optional Mehrere Themen auf einmal.

Antwort

201 Angelegt – ein Themen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 409 conflict 422 validation_error 429 rate_limited

POST /seinfeld/topics
curl -X POST https://persolz.com/api/v1/seinfeld/topics \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "topic": "Der verlorene Schlüssel",
    "priority": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/topics');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'topic' => 'Der verlorene Schlüssel',
        'priority' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/topics', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    topic: 'Der verlorene Schlüssel',
    priority: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/topics",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "topic": "Der verlorene Schlüssel",
        "priority": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 15,
    "topic": "Der verlorene Schlüssel",
    "notes": "Bezug zu Ordnungssystemen",
    "priority": true,
    "status": "open",
    "used_at": null,
    "email_id": null,
    "sort_order": 3,
    "created_at": "2026-10-01T09:00:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Thema ändern

PATCH /api/v1/seinfeld/topics/{id}
Scope write Tarif: seinfeld

Ändert Text, Notizen, Stern oder Status (open/skipped).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
topic string optional Thema.
notes string optional Notizen.
priority boolean optional Stern.
status string optional open | skipped

Antwort

200 OK – ein Themen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

PATCH /seinfeld/topics/{id}
curl -X PATCH https://persolz.com/api/v1/seinfeld/topics/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "priority": false
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/topics/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'priority' => false,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/topics/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    priority: false
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/seinfeld/topics/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "priority": False,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 15,
    "topic": "Der verlorene Schlüssel",
    "notes": "Bezug zu Ordnungssystemen",
    "priority": true,
    "status": "open",
    "used_at": null,
    "email_id": null,
    "sort_order": 3,
    "created_at": "2026-10-01T09:00:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Thema löschen

DELETE /api/v1/seinfeld/topics/{id}
Scope write Tarif: seinfeld

Löscht das Thema.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /seinfeld/topics/{id}
curl -X DELETE https://persolz.com/api/v1/seinfeld/topics/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/topics/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/seinfeld/topics/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/seinfeld/topics/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Seinfeld-Mails auflisten

GET /api/v1/seinfeld/emails
Scope read Tarif: seinfeld paginiert

Entwürfe, geplante und versendete Seinfeld-Mails.

Query-Parameter

NameBeschreibung
status string optional all | pending (KI-Entwürfe zur Freigabe) | draft | planned (freigegeben & geplant, Alias „scheduled“) | sent | problems (fehlgeschlagen / abgebrochen) Werte: all, pending, draft, planned, sent, problems Beispiel: planned
q string optional Suche in Betreff und Story. Beispiel: kaffee
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Seinfeld-Mail-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /seinfeld/emails
curl "https://persolz.com/api/v1/seinfeld/emails?status=planned" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'planned',
]);
$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'planned'
});
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/seinfeld/emails",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "planned",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 120,
      "subject": "Was mein Kaffeebecher über Marketing weiß",
      "preheader": "Eine Geschichte über Gewohnheiten",
      "html": "<!doctype html><html>…</html>",
      "status": "scheduled",
      "status_label": "Geplant",
      "ai_generated": true,
      "ai_model": "claude-sonnet-5-5",
      "topic_id": 15,
      "arc_id": null,
      "episode_no": null,
      "story_title": "Der Kaffeebecher",
      "story_summary": "Warum kleine Rituale Vertrauen schaffen.",
      "keywords": ["kaffee", "gewohnheit"],
      "template_key": "seinfeld-01",
      "scheduled_at": "2026-10-10T09:00:00+02:00",
      "sent_at": null,
      "campaign_id": 901,
      "smtp_account_id": null,
      "audience": null,
      "attachment_ids": [],
      "include_default_attachments": false,
      "created_at": "2026-10-09T07:00:00+02:00",
      "updated_at": "2026-10-09T08:12:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Seinfeld-Mail anlegen

POST /api/v1/seinfeld/emails
Scope write Tarif: seinfeld Idempotency-Key

Legt einen Entwurf an (wird nicht automatisch versendet – plane ihn mit /schedule).

Request-Body application/json

FeldBeschreibung
subject string optional Betreff.
preheader string optional Vorschautext.
html string optional HTML-Inhalt.
scheduled_at string optional Geplanter Versand (ISO 8601); leer = nächster freier Termin beim Planen.
topic_id integer optional Thema aus der Themenliste (wird als benutzt markiert).
arc_id integer optional Story-Bogen.
story_title string optional Titel der Geschichte (Story-Gedächtnis).
story_summary string optional Kurze Zusammenfassung (Story-Gedächtnis gegen Wiederholungen).
keywords array<string> optional Stichworte.
smtp_account_id integer optional SMTP-Konto (Standard: Einstellungen).
audience object optional Abweichende Empfänger (Standard: Einstellungen).
attachment_ids array<integer> optional Anhänge.
template_key string optional Design-Vorlage.

Antwort

201 Angelegt – ein Seinfeld-Mail-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 422 validation_error 429 rate_limited

POST /seinfeld/emails
curl -X POST https://persolz.com/api/v1/seinfeld/emails \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "html": "<p>Heute Morgen …</p>",
    "topic_id": 15
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'subject' => 'Was mein Kaffeebecher über Marketing weiß',
        'html' => '<p>Heute Morgen …</p>',
        'topic_id' => 15,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    subject: 'Was mein Kaffeebecher über Marketing weiß',
    html: '<p>Heute Morgen …</p>',
    topic_id: 15
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/emails",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "subject": "Was mein Kaffeebecher über Marketing weiß",
        "html": "<p>Heute Morgen …</p>",
        "topic_id": 15,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "status": "draft",
    "status_label": "Entwurf",
    "campaign_id": null,
    "ai_generated": false,
    "id": 120,
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "preheader": "Eine Geschichte über Gewohnheiten",
    "html": "<!doctype html><html>…</html>",
    "ai_model": "claude-sonnet-5-5",
    "topic_id": 15,
    "arc_id": null,
    "episode_no": null,
    "story_title": "Der Kaffeebecher",
    "story_summary": "Warum kleine Rituale Vertrauen schaffen.",
    "keywords": ["kaffee", "gewohnheit"],
    "template_key": "seinfeld-01",
    "scheduled_at": "2026-10-10T09:00:00+02:00",
    "sent_at": null,
    "smtp_account_id": null,
    "audience": null,
    "attachment_ids": [],
    "include_default_attachments": false,
    "created_at": "2026-10-09T07:00:00+02:00",
    "updated_at": "2026-10-09T08:12:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seinfeld-Mail abrufen

GET /api/v1/seinfeld/emails/{id}
Scope read Tarif: seinfeld

Eine Seinfeld-Mail mit HTML und – nach dem Versand – Kennzahlen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Seinfeld-Mail-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /seinfeld/emails/{id}
curl https://persolz.com/api/v1/seinfeld/emails/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/seinfeld/emails/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 120,
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "preheader": "Eine Geschichte über Gewohnheiten",
    "html": "<!doctype html><html>…</html>",
    "status": "scheduled",
    "status_label": "Geplant",
    "ai_generated": true,
    "ai_model": "claude-sonnet-5-5",
    "topic_id": 15,
    "arc_id": null,
    "episode_no": null,
    "story_title": "Der Kaffeebecher",
    "story_summary": "Warum kleine Rituale Vertrauen schaffen.",
    "keywords": ["kaffee", "gewohnheit"],
    "template_key": "seinfeld-01",
    "scheduled_at": "2026-10-10T09:00:00+02:00",
    "sent_at": null,
    "campaign_id": 901,
    "smtp_account_id": null,
    "audience": null,
    "attachment_ids": [],
    "include_default_attachments": false,
    "created_at": "2026-10-09T07:00:00+02:00",
    "updated_at": "2026-10-09T08:12:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seinfeld-Mail ändern

PATCH /api/v1/seinfeld/emails/{id}
Scope write Tarif: seinfeld

Ändert die übergebenen Felder. Versendete Mails sind gesperrt (409) – nur das Story-Gedächtnis bleibt änderbar. Geplante Mails werden neu eingeplant.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
subject string optional Betreff.
preheader string optional Vorschautext.
html string optional HTML-Inhalt.
scheduled_at string optional Geplanter Versand (ISO 8601); leer = nächster freier Termin beim Planen.
topic_id integer optional Thema aus der Themenliste (wird als benutzt markiert).
arc_id integer optional Story-Bogen.
story_title string optional Titel der Geschichte (Story-Gedächtnis).
story_summary string optional Kurze Zusammenfassung (Story-Gedächtnis gegen Wiederholungen).
keywords array<string> optional Stichworte.
smtp_account_id integer optional SMTP-Konto (Standard: Einstellungen).
audience object optional Abweichende Empfänger (Standard: Einstellungen).
attachment_ids array<integer> optional Anhänge.
template_key string optional Design-Vorlage.

Antwort

200 OK – ein Seinfeld-Mail-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

PATCH /seinfeld/emails/{id}
curl -X PATCH https://persolz.com/api/v1/seinfeld/emails/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Neuer Betreff"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'subject' => 'Neuer Betreff',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    subject: 'Neuer Betreff'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/seinfeld/emails/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "subject": "Neuer Betreff",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 120,
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "preheader": "Eine Geschichte über Gewohnheiten",
    "html": "<!doctype html><html>…</html>",
    "status": "scheduled",
    "status_label": "Geplant",
    "ai_generated": true,
    "ai_model": "claude-sonnet-5-5",
    "topic_id": 15,
    "arc_id": null,
    "episode_no": null,
    "story_title": "Der Kaffeebecher",
    "story_summary": "Warum kleine Rituale Vertrauen schaffen.",
    "keywords": ["kaffee", "gewohnheit"],
    "template_key": "seinfeld-01",
    "scheduled_at": "2026-10-10T09:00:00+02:00",
    "sent_at": null,
    "campaign_id": 901,
    "smtp_account_id": null,
    "audience": null,
    "attachment_ids": [],
    "include_default_attachments": false,
    "created_at": "2026-10-09T07:00:00+02:00",
    "updated_at": "2026-10-09T08:12:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seinfeld-Mail löschen

DELETE /api/v1/seinfeld/emails/{id}
Scope write Tarif: seinfeld

Löscht die Mail (das Thema wird wieder frei). Laufende Versände → 409.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 429 rate_limited

DELETE /seinfeld/emails/{id}
curl -X DELETE https://persolz.com/api/v1/seinfeld/emails/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/seinfeld/emails/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Seinfeld-Mail freigeben & planen

POST /api/v1/seinfeld/emails/{id}/schedule
Scope send Tarif: seinfeld Idempotency-Key

Gibt die Mail frei und plant sie zum angegebenen Zeitpunkt, sonst zum geplanten Datum oder zum nächsten freien Termin im Rhythmus.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
scheduled_at string optional Zeitpunkt (ISO 8601), optional.

Antwort

200 OK – ein Seinfeld-Mail-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

POST /seinfeld/emails/{id}/schedule
curl -X POST https://persolz.com/api/v1/seinfeld/emails/42/schedule \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "scheduled_at": "2026-10-10T09:00:00+02:00"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails/42/schedule');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'scheduled_at' => '2026-10-10T09:00:00+02:00',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails/42/schedule', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    scheduled_at: '2026-10-10T09:00:00+02:00'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/emails/42/schedule",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "scheduled_at": "2026-10-10T09:00:00+02:00",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "status": "scheduled",
    "scheduled_at": "2026-10-10T09:00:00+02:00",
    "campaign_id": 901,
    "id": 120,
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "preheader": "Eine Geschichte über Gewohnheiten",
    "html": "<!doctype html><html>…</html>",
    "status_label": "Geplant",
    "ai_generated": true,
    "ai_model": "claude-sonnet-5-5",
    "topic_id": 15,
    "arc_id": null,
    "episode_no": null,
    "story_title": "Der Kaffeebecher",
    "story_summary": "Warum kleine Rituale Vertrauen schaffen.",
    "keywords": ["kaffee", "gewohnheit"],
    "template_key": "seinfeld-01",
    "sent_at": null,
    "smtp_account_id": null,
    "audience": null,
    "attachment_ids": [],
    "include_default_attachments": false,
    "created_at": "2026-10-09T07:00:00+02:00",
    "updated_at": "2026-10-09T08:12:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Planung aufheben

POST /api/v1/seinfeld/emails/{id}/unschedule
Scope write Tarif: seinfeld Idempotency-Key

Setzt eine geplante Mail zurück auf Entwurf (das Datum bleibt erhalten). Laufende Versände → 409.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Seinfeld-Mail-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 429 rate_limited

POST /seinfeld/emails/{id}/unschedule
curl -X POST https://persolz.com/api/v1/seinfeld/emails/42/unschedule \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/emails/42/unschedule');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/emails/42/unschedule', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/emails/42/unschedule",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "status": "draft",
    "status_label": "Entwurf",
    "id": 120,
    "subject": "Was mein Kaffeebecher über Marketing weiß",
    "preheader": "Eine Geschichte über Gewohnheiten",
    "html": "<!doctype html><html>…</html>",
    "ai_generated": true,
    "ai_model": "claude-sonnet-5-5",
    "topic_id": 15,
    "arc_id": null,
    "episode_no": null,
    "story_title": "Der Kaffeebecher",
    "story_summary": "Warum kleine Rituale Vertrauen schaffen.",
    "keywords": ["kaffee", "gewohnheit"],
    "template_key": "seinfeld-01",
    "scheduled_at": "2026-10-10T09:00:00+02:00",
    "sent_at": null,
    "campaign_id": 901,
    "smtp_account_id": null,
    "audience": null,
    "attachment_ids": [],
    "include_default_attachments": false,
    "created_at": "2026-10-09T07:00:00+02:00",
    "updated_at": "2026-10-09T08:12:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seinfeld-Mail mit KI schreiben

POST /api/v1/seinfeld/generate
Scope ai Tarif: seinfeld Tarif: ai Idempotency-Key

Die KI schreibt eine Seinfeld-Mail aus dem nächsten Thema, einem Story-Bogen, einem bestimmten Thema oder Deiner Idee – mit Story-Gedächtnis gegen Wiederholungen. Mit persist=true wird ein Entwurf gespeichert (Antwort enthält email_id). Verbraucht KI-Guthaben.

Request-Body application/json

FeldBeschreibung
source string optional auto | topic | next_topic | arc | idea Werte: auto, topic, next_topic, arc, idea
topic_id integer optional Bei source=topic.
arc_id integer optional Bei source=arc.
idea string optional Bei source=idea: Deine Idee in ein paar Sätzen.
model string optional sonnet (Qualität) | haiku (schnell & günstig)
date string optional Geplantes Versanddatum (für Bezüge im Text).
persist boolean optional Als Entwurf speichern.
scheduled_at string optional Bei persist: geplanter Zeitpunkt.

Antwort

200 OK

Alle 10 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
subject string Betreff.
preheader string Vorschautext (erscheint im Postfach neben dem Betreff).
html string HTML-Inhalt.
story_title string Titel der Geschichte (Story-Gedächtnis gegen Wiederholungen).
story_summary string Kurze Zusammenfassung (Story-Gedächtnis).
keywords array<string> Stichworte.
model string Verwendetes KI-Modell.
cost_eur number Kosten dieser KI-Anfrage in Euro (wird vom KI-Guthaben abgezogen).
email_id integer ID des gespeicherten Entwurfs (bei persist=true) oder null.
warning string · null Hinweis (z. B. wenn die KI gekürzt hat) oder null.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited 503 service_unavailable

POST /seinfeld/generate
curl -X POST https://persolz.com/api/v1/seinfeld/generate \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "idea",
    "idea": "Mein Hund hat heute die Post gefressen",
    "persist": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/seinfeld/generate');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'source' => 'idea',
        'idea' => 'Mein Hund hat heute die Post gefressen',
        'persist' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/seinfeld/generate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    source: 'idea',
    idea: 'Mein Hund hat heute die Post gefressen',
    persist: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/seinfeld/generate",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "source": "idea",
        "idea": "Mein Hund hat heute die Post gefressen",
        "persist": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "subject": "Der Tag, an dem Bruno die Post fraß",
    "preheader": "Und warum das gut fürs Geschäft war",
    "html": "<!doctype html><html>…</html>",
    "story_title": "Bruno und die Post",
    "story_summary": "…",
    "keywords": ["hund", "post"],
    "model": "claude-sonnet-5-5",
    "cost_eur": 0.0123,
    "email_id": 121,
    "warning": null
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Termine im Rhythmus

GET /api/v1/seinfeld/slots
Scope read Tarif: seinfeld

Alle Versandtermine im Zeitraum mit zugeordneter Mail oder Lücke (is_gap).

Query-Parameter

NameBeschreibung
from string optional Startdatum YYYY-MM-DD (Standard heute). Beispiel: 2026-10-09
to string optional Enddatum YYYY-MM-DD (Standard +14 Tage, max. 120 Tage). Beispiel: 2026-10-23

Antwort

200 OK – eine Liste, jeder Eintrag mit diesen Feldern:

Alle 11 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
date string (Datum) Datum (YYYY-MM-DD).
time string Uhrzeit (HH:MM).
datetime string (Datum/Zeit) Termin als Zeitpunkt (ISO 8601).
email object Zugeordnete Seinfeld-Mail oder null (Lücke).
email.id integer ID der Seinfeld-Mail.
email.subject string Betreff.
email.status string Status der Mail.
is_gap boolean Für diesen Termin gibt es noch keine Mail.
is_missed boolean Termin liegt in der Vergangenheit, ohne dass etwas versendet wurde.
is_extra boolean Zusätzliche Mail außerhalb des Rhythmus.
weekday string Wochentag (Mo–So).

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /seinfeld/slots
curl "https://persolz.com/api/v1/seinfeld/slots?from=2026-10-09&to=2026-10-23" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'from' => '2026-10-09',
    'to' => '2026-10-23',
]);
$ch = curl_init('https://persolz.com/api/v1/seinfeld/slots?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  from: '2026-10-09',
  to: '2026-10-23'
});
const response = await fetch('https://persolz.com/api/v1/seinfeld/slots?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/seinfeld/slots",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "from": "2026-10-09",
        "to": "2026-10-23",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "date": "2026-10-10",
      "time": "09:00",
      "datetime": "2026-10-10T09:00:00+02:00",
      "email": {
        "id": 120,
        "subject": "Was mein Kaffeebecher über Marketing weiß",
        "status": "scheduled"
      },
      "is_gap": false,
      "is_missed": false,
      "is_extra": false,
      "weekday": "Sa"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Seifenopern

Story-Sequenzen (Soap-Opera-Sequenzen): Tage, Teilnehmer, Aktivierung und KI-Gliederung.

Eine Seifenoper ist eine Story-Sequenz über mehrere Tage. Kontakte nehmen Teil, sobald sie aktiv ist und sie aufgenommen wurden (per API, Liste oder Integration).

Objekte: Seifenopern-Objekt · Tages-Objekt · Teilnahme-Objekt

Das Seifenopern-Objekt

Eine Seifenoper mit Auslöser, Versandzeit und Kennzahlen.

Attribute

Das Seifenopern-Objekt
FeldBeschreibung
id integer Eindeutige ID.
name string Name der Seifenoper.
description string Beschreibung.
goal string Ziel bzw. Angebot der Sequenz.
status string draft (Entwurf), active (aktiv), paused (pausiert) oder archived (archiviert).
status_label string Status in Worten (Deutsch).
trigger_type string Auslöser: manual (nur manuell/API), list_join (beim Eintrag in eine Liste) oder api.
trigger_list_id integer Liste für den Auslöser list_join oder null.
trigger_label string Auslöser in Worten.
send_time string Versandzeit (HH:MM, Europe/Berlin).
skip_weekends boolean Samstag und Sonntag werden übersprungen.
smtp_account_id integer · null SMTP-Konto für den Versand oder null (= Standardkonto des Moduls).
ai_model string KI-Modell: sonnet (beste Qualität) oder haiku (schnell & günstig).
template_key string Design-Vorlage aus der Vorlagen-Bibliothek (z. B. seinfeld-01) oder null.
include_default_attachments boolean Standard-Anhänge werden zusätzlich mitgesendet.
step_count integer Anzahl Tage.
enrolled_active integer Aktive Teilnehmer.
enrolled_completed integer Abgeschlossene Teilnahmen.
sent integer Insgesamt versendete Mails.
open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Seifenopern-Objekt JSON
{
  "id": 7,
  "name": "Willkommen bei Kartenherz",
  "description": "5 Tage Story-Sequenz für neue Leads",
  "goal": "Verkauf des Kartenset-Abos",
  "status": "active",
  "status_label": "Aktiv",
  "trigger_type": "list_join",
  "trigger_list_id": 3,
  "trigger_label": "Beim Eintrag in „Kartenherz Funnel“",
  "send_time": "09:00",
  "skip_weekends": false,
  "smtp_account_id": null,
  "ai_model": "sonnet",
  "template_key": "soap-01",
  "include_default_attachments": false,
  "step_count": 5,
  "enrolled_active": 312,
  "enrolled_completed": 845,
  "sent": 4980,
  "open_rate": 0.61,
  "click_rate": 0.14,
  "created_at": "2026-09-01T10:00:00+02:00",
  "updated_at": "2026-10-01T08:30:00+02:00"
}

Das Tages-Objekt

Attribute

Das Tages-Objekt
FeldBeschreibung
id integer Eindeutige ID.
sequence_id integer ID der Seifenoper.
position integer Reihenfolge (ab 1).
day integer Versandtag (1 = Starttag).
is_active boolean Tag ist aktiv – inaktive Tage werden übersprungen.
stage string Etappe: buehne, drama, epiphanie, vorteile, dringlichkeit, einwaende, szenario, bonus, faq, letzte_chance oder custom.
stage_label string Etappe in Worten.
subject string Betreff.
preheader string Vorschautext (erscheint im Postfach neben dem Betreff).
html string HTML-Inhalt.
attachment_ids array IDs der Anhänge aus der Anhang-Cloud.
include_default_attachments boolean Standard-Anhänge werden zusätzlich mitgesendet.
campaign_id integer Interne Kampagne dieses Tages (für Statistiken).
ab_test_id integer · null ID des verknüpften A/B-Tests oder null.
stats object Kennzahlen dieses Tages.
stats.sent integer Versendet.
stats.opens integer Öffnungen gesamt (inkl. mehrfacher Öffnungen).
stats.clicks integer Klicks gesamt.
stats.unsubs integer Abmeldungen.
stats.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
stats.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Tages-Objekt JSON
{
  "id": 31,
  "sequence_id": 7,
  "position": 1,
  "day": 1,
  "is_active": true,
  "stage": "buehne",
  "stage_label": "Bühne bereiten",
  "subject": "Wie alles mit einer Postkarte begann",
  "preheader": "Tag 1 von 5",
  "html": "<!doctype html><html>…</html>",
  "attachment_ids": [],
  "include_default_attachments": false,
  "campaign_id": 950,
  "ab_test_id": null,
  "stats": {
    "sent": 1157,
    "opens": 760,
    "clicks": 98,
    "unsubs": 4,
    "open_rate": 0.6569,
    "click_rate": 0.0847
  },
  "created_at": "2026-09-01T10:00:00+02:00",
  "updated_at": "2026-09-02T11:00:00+02:00"
}

Das Teilnahme-Objekt

Attribute

Das Teilnahme-Objekt
FeldBeschreibung
id integer Eindeutige ID.
contact_id integer ID des Kontakts.
email string E-Mail-Adresse.
name string Name des Kontakts.
status string active, paused, completed, cancelled oder unsubscribed.
status_label string Status in Worten (Deutsch).
source string Wie der Kontakt aufgenommen wurde (api, list, manual, digistore …).
start_date string (Datum) Startdatum der Seifenoper.
current_position integer Position des zuletzt erreichten Tages.
current_day integer Aktueller Versandtag.
total_steps integer Anzahl Tage der Sequenz.
progress number Fortschritt von 0 bis 1.
next_send_at string (Datum/Zeit) Nächster geplanter Versand oder null.
last_sent_at string (Datum/Zeit) Letzter Versand an den Kontakt oder null.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
Das Teilnahme-Objekt JSON
{
  "id": 501,
  "contact_id": 42,
  "email": "anna@beispiel.de",
  "name": "Anna Schmidt",
  "status": "active",
  "status_label": "Aktiv",
  "source": "api",
  "start_date": "2026-10-08",
  "current_position": 2,
  "current_day": 2,
  "total_steps": 5,
  "progress": 0.4,
  "next_send_at": "2026-10-10T09:00:00+02:00",
  "last_sent_at": "2026-10-09T09:00:12+02:00",
  "created_at": "2026-10-08T12:00:00+02:00"
}

Seifenopern auflisten

GET /api/v1/sequences
Scope read Tarif: soap paginiert

Alle Sequenzen mit Kennzahlen (Tage, Teilnehmer, Öffnungs-/Klickrate).

Query-Parameter

NameBeschreibung
status string optional draft | active | paused | archived Werte: draft, active, paused, archived Beispiel: active
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Seifenopern-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /sequences
curl "https://persolz.com/api/v1/sequences?status=active" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'active',
]);
$ch = curl_init('https://persolz.com/api/v1/sequences?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'active'
});
const response = await fetch('https://persolz.com/api/v1/sequences?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/sequences",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "active",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 7,
      "name": "Willkommen bei Kartenherz",
      "description": "5 Tage Story-Sequenz für neue Leads",
      "goal": "Verkauf des Kartenset-Abos",
      "status": "active",
      "status_label": "Aktiv",
      "trigger_type": "list_join",
      "trigger_list_id": 3,
      "trigger_label": "Beim Eintrag in „Kartenherz Funnel“",
      "send_time": "09:00",
      "skip_weekends": false,
      "smtp_account_id": null,
      "ai_model": "sonnet",
      "template_key": "soap-01",
      "include_default_attachments": false,
      "step_count": 5,
      "enrolled_active": 312,
      "enrolled_completed": 845,
      "sent": 4980,
      "open_rate": 0.61,
      "click_rate": 0.14,
      "created_at": "2026-09-01T10:00:00+02:00",
      "updated_at": "2026-10-01T08:30:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Seifenoper anlegen

POST /api/v1/sequences
Scope write Tarif: soap Idempotency-Key

Legt eine Sequenz als Entwurf an (optional mit 5 vorbereiteten Tagen). Free-Tarif: 1 Sequenz.

Request-Body application/json

FeldBeschreibung
name string Pflicht Name der Seifenoper.
description string optional Beschreibung.
goal string optional Ziel / Angebot.
trigger_type string optional manual | list_join | api Werte: manual, list_join, api
trigger_list_id integer optional Bei list_join: Liste, deren neue Mitglieder automatisch starten.
send_time string optional Versandzeit HH:MM.
skip_weekends boolean optional Wochenenden überspringen.
smtp_account_id integer optional SMTP-Konto.
ai_model string optional sonnet | haiku
template_key string optional Design-Vorlage (soap-…).
include_default_attachments boolean optional Standard-Anhänge mitsenden.
starter string optional Beim Anlegen: hints (5 vorbereitete Tage mit Schreibhinweisen), library (Beispieltexte der Vorlagen-Serie) oder none. Werte: hints, library, none

Antwort

201 Angelegt – ein Seifenopern-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited

POST /sequences
curl -X POST https://persolz.com/api/v1/sequences \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Willkommen bei Kartenherz",
    "trigger_type": "list_join",
    "trigger_list_id": 3,
    "starter": "hints"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'name' => 'Willkommen bei Kartenherz',
        'trigger_type' => 'list_join',
        'trigger_list_id' => 3,
        'starter' => 'hints',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Willkommen bei Kartenherz',
    trigger_type: 'list_join',
    trigger_list_id: 3,
    starter: 'hints'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/sequences",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "name": "Willkommen bei Kartenherz",
        "trigger_type": "list_join",
        "trigger_list_id": 3,
        "starter": "hints",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "status": "draft",
    "status_label": "Entwurf",
    "id": 7,
    "name": "Willkommen bei Kartenherz",
    "description": "5 Tage Story-Sequenz für neue Leads",
    "goal": "Verkauf des Kartenset-Abos",
    "trigger_type": "list_join",
    "trigger_list_id": 3,
    "trigger_label": "Beim Eintrag in „Kartenherz Funnel“",
    "send_time": "09:00",
    "skip_weekends": false,
    "smtp_account_id": null,
    "ai_model": "sonnet",
    "template_key": "soap-01",
    "include_default_attachments": false,
    "step_count": 5,
    "enrolled_active": 312,
    "enrolled_completed": 845,
    "sent": 4980,
    "open_rate": 0.61,
    "click_rate": 0.14,
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Gliederung mit KI planen

POST /api/v1/sequences/generate
Scope ai Tarif: soap Tarif: ai Idempotency-Key

Die KI plant eine Seifenoper (3–10 Tage) aus Deinem Briefing: je Tag Etappe, Betreff, Inhalt und Cliffhanger. Mit create=true wird direkt eine Sequenz (Entwurf) mit diesen Tagen angelegt. Verbraucht KI-Guthaben.

Request-Body application/json

FeldBeschreibung
product string Pflicht Was verkaufst Du?
audience string Pflicht Für wen?
story string Pflicht Deine Geschichte (Hintergrund, Tiefpunkt).
epiphany string optional Der Aha-Moment.
objections string optional Typische Einwände.
url string optional Link zum Angebot.
tone string optional Tonalität.
days integer optional Anzahl Tage (3–10, Standard 5).
model string optional sonnet | haiku
create boolean optional Sequenz direkt anlegen.
name string optional Name der Sequenz bei create.

Antwort

200 OK

Alle 10 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
title string Titel der geplanten Story.
days array<object> Die geplanten Tage.
days[].day integer Versandtag.
days[].stage string Etappe.
days[].subject string Betreffvorschlag.
days[].summary string Inhalt in Kurzform.
days[].hook string Cliffhanger zum nächsten Tag.
warning string · null Hinweis (z. B. wenn die KI gekürzt hat) oder null.
cost_eur number Kosten dieser KI-Anfrage in Euro (wird vom KI-Guthaben abgezogen).
sequence_id integer · null ID der angelegten Sequenz (bei create=true) oder null.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited 503 service_unavailable

POST /sequences/generate
curl -X POST https://persolz.com/api/v1/sequences/generate \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "Kartenset-Abo",
    "audience": "Kartenliebhaber",
    "story": "Ich habe als Illustrator fast aufgegeben, bis …",
    "days": 5
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/generate');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'product' => 'Kartenset-Abo',
        'audience' => 'Kartenliebhaber',
        'story' => 'Ich habe als Illustrator fast aufgegeben, bis …',
        'days' => 5,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/generate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    product: 'Kartenset-Abo',
    audience: 'Kartenliebhaber',
    story: 'Ich habe als Illustrator fast aufgegeben, bis …',
    days: 5
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/sequences/generate",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "product": "Kartenset-Abo",
        "audience": "Kartenliebhaber",
        "story": "Ich habe als Illustrator fast aufgegeben, bis …",
        "days": 5,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "title": "Von der leeren Mappe zum Abo",
    "days": [
      {
        "day": 1,
        "stage": "buehne",
        "subject": "Wie alles mit einer Postkarte begann",
        "summary": "…",
        "hook": "Morgen erzähle ich Dir, was dann schiefging."
      }
    ],
    "warning": null,
    "cost_eur": 0.021,
    "sequence_id": null
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seifenoper abrufen

GET /api/v1/sequences/{id}
Scope read Tarif: soap

Sequenz mit Tagen (ohne HTML) und Statistik.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Seifenopern-Objekt, zusätzlich:

Antwortfelder
FeldBeschreibung
steps array<Tages-Objekt> Tage der Sequenz (ohne HTML). Tages-Objekt
stats object Kennzahlen.
stats.sent integer Versendet.
stats.opens integer Öffnungen gesamt (inkl. mehrfacher Öffnungen).
stats.clicks integer Klicks gesamt.
stats.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
stats.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
stats.completion_rate number Anteil der Teilnehmer, die alle Tage erhalten haben (0 bis 1).

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /sequences/{id}
curl https://persolz.com/api/v1/sequences/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/sequences/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 7,
    "name": "Willkommen bei Kartenherz",
    "description": "5 Tage Story-Sequenz für neue Leads",
    "goal": "Verkauf des Kartenset-Abos",
    "status": "active",
    "status_label": "Aktiv",
    "trigger_type": "list_join",
    "trigger_list_id": 3,
    "trigger_label": "Beim Eintrag in „Kartenherz Funnel“",
    "send_time": "09:00",
    "skip_weekends": false,
    "smtp_account_id": null,
    "ai_model": "sonnet",
    "template_key": "soap-01",
    "include_default_attachments": false,
    "step_count": 5,
    "enrolled_active": 312,
    "enrolled_completed": 845,
    "sent": 4980,
    "open_rate": 0.61,
    "click_rate": 0.14,
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00",
    "steps": [
      {
        "id": 31,
        "sequence_id": 7,
        "position": 1,
        "day": 1,
        "is_active": true,
        "stage": "buehne",
        "stage_label": "Bühne bereiten",
        "subject": "Wie alles mit einer Postkarte begann",
        "preheader": "Tag 1 von 5",
        "attachment_ids": [],
        "include_default_attachments": false,
        "campaign_id": 950,
        "ab_test_id": null,
        "stats": {
          "sent": 1157,
          "opens": 760,
          "clicks": 98,
          "unsubs": 4,
          "open_rate": 0.6569,
          "click_rate": 0.0847
        },
        "created_at": "2026-09-01T10:00:00+02:00",
        "updated_at": "2026-09-02T11:00:00+02:00"
      }
    ],
    "stats": {
      "sent": 4980,
      "opens": 3038,
      "clicks": 697,
      "open_rate": 0.61,
      "click_rate": 0.14,
      "completion_rate": 0.73
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seifenoper ändern

PATCH /api/v1/sequences/{id}
Scope write Tarif: soap

Ändert die übergebenen Einstellungen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
name string Pflicht Name der Seifenoper.
description string optional Beschreibung.
goal string optional Ziel / Angebot.
trigger_type string optional manual | list_join | api Werte: manual, list_join, api
trigger_list_id integer optional Bei list_join: Liste, deren neue Mitglieder automatisch starten.
send_time string optional Versandzeit HH:MM.
skip_weekends boolean optional Wochenenden überspringen.
smtp_account_id integer optional SMTP-Konto.
ai_model string optional sonnet | haiku
template_key string optional Design-Vorlage (soap-…).
include_default_attachments boolean optional Standard-Anhänge mitsenden.
starter string optional Beim Anlegen: hints (5 vorbereitete Tage mit Schreibhinweisen), library (Beispieltexte der Vorlagen-Serie) oder none. Werte: hints, library, none

Antwort

200 OK – ein Seifenopern-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

PATCH /sequences/{id}
curl -X PATCH https://persolz.com/api/v1/sequences/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "send_time": "08:30",
    "skip_weekends": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'send_time' => '08:30',
        'skip_weekends' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    send_time: '08:30',
    skip_weekends: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/sequences/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "send_time": "08:30",
        "skip_weekends": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 7,
    "name": "Willkommen bei Kartenherz",
    "description": "5 Tage Story-Sequenz für neue Leads",
    "goal": "Verkauf des Kartenset-Abos",
    "status": "active",
    "status_label": "Aktiv",
    "trigger_type": "list_join",
    "trigger_list_id": 3,
    "trigger_label": "Beim Eintrag in „Kartenherz Funnel“",
    "send_time": "09:00",
    "skip_weekends": false,
    "smtp_account_id": null,
    "ai_model": "sonnet",
    "template_key": "soap-01",
    "include_default_attachments": false,
    "step_count": 5,
    "enrolled_active": 312,
    "enrolled_completed": 845,
    "sent": 4980,
    "open_rate": 0.61,
    "click_rate": 0.14,
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seifenoper löschen

DELETE /api/v1/sequences/{id}
Scope write Tarif: soap

Löscht die Sequenz mit allen Tagen, Statistiken und Teilnahmen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /sequences/{id}
curl -X DELETE https://persolz.com/api/v1/sequences/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/sequences/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/sequences/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Tage auflisten

GET /api/v1/sequences/{id}/steps
Scope read Tarif: soap

Alle Tage der Sequenz in Reihenfolge, inkl. HTML und Kennzahlen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – Liste von Tages-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /sequences/{id}/steps
curl https://persolz.com/api/v1/sequences/42/steps \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/steps');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42/steps', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/sequences/42/steps",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 31,
      "sequence_id": 7,
      "position": 1,
      "day": 1,
      "is_active": true,
      "stage": "buehne",
      "stage_label": "Bühne bereiten",
      "subject": "Wie alles mit einer Postkarte begann",
      "preheader": "Tag 1 von 5",
      "html": "<!doctype html><html>…</html>",
      "attachment_ids": [],
      "include_default_attachments": false,
      "campaign_id": 950,
      "ab_test_id": null,
      "stats": {
        "sent": 1157,
        "opens": 760,
        "clicks": 98,
        "unsubs": 4,
        "open_rate": 0.6569,
        "click_rate": 0.0847
      },
      "created_at": "2026-09-01T10:00:00+02:00",
      "updated_at": "2026-09-02T11:00:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Tag hinzufügen

POST /api/v1/sequences/{id}/steps
Scope write Tarif: soap Idempotency-Key

Hängt einen Tag an (oder fügt ihn hinter after_step_id ein). Max. 60 Tage.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
subject string optional Betreff.
preheader string optional Vorschautext.
html string optional HTML-Inhalt.
day integer optional Versandtag (1 = Starttag, max. 365). Spätere Tage werden bei Bedarf verschoben.
stage string optional Etappe: buehne, drama, epiphanie, vorteile, dringlichkeit, einwaende, szenario, bonus, faq, letzte_chance, custom
is_active boolean optional Aktiv (ausgesetzte Tage werden übersprungen).
attachment_ids array<integer> optional Anhänge.
include_default_attachments boolean optional Standard-Anhänge.
after_step_id integer optional Nur beim Anlegen: hinter diesem Tag einfügen (Standard: ans Ende).

Antwort

201 Angelegt – ein Tages-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /sequences/{id}/steps
curl -X POST https://persolz.com/api/v1/sequences/42/steps \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Das Drama beginnt",
    "html": "<p>Gestern habe ich Dir erzählt …</p>",
    "day": 2,
    "stage": "drama"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/steps');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'subject' => 'Das Drama beginnt',
        'html' => '<p>Gestern habe ich Dir erzählt …</p>',
        'day' => 2,
        'stage' => 'drama',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42/steps', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    subject: 'Das Drama beginnt',
    html: '<p>Gestern habe ich Dir erzählt …</p>',
    day: 2,
    stage: 'drama'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/sequences/42/steps",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "subject": "Das Drama beginnt",
        "html": "<p>Gestern habe ich Dir erzählt …</p>",
        "day": 2,
        "stage": "drama",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 31,
    "sequence_id": 7,
    "position": 1,
    "day": 1,
    "is_active": true,
    "stage": "buehne",
    "stage_label": "Bühne bereiten",
    "subject": "Wie alles mit einer Postkarte begann",
    "preheader": "Tag 1 von 5",
    "html": "<!doctype html><html>…</html>",
    "attachment_ids": [],
    "include_default_attachments": false,
    "campaign_id": 950,
    "ab_test_id": null,
    "stats": {
      "sent": 1157,
      "opens": 760,
      "clicks": 98,
      "unsubs": 4,
      "open_rate": 0.6569,
      "click_rate": 0.0847
    },
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-09-02T11:00:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Tag ändern

PATCH /api/v1/sequences/{id}/steps/{stepId}
Scope write Tarif: soap

Ändert einen Tag. Ein neuer „day“ verschiebt bei Bedarf die folgenden Tage.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource
stepId integer Pflicht ID des Tages

Request-Body application/json

FeldBeschreibung
subject string optional Betreff.
preheader string optional Vorschautext.
html string optional HTML-Inhalt.
day integer optional Versandtag (1 = Starttag, max. 365). Spätere Tage werden bei Bedarf verschoben.
stage string optional Etappe: buehne, drama, epiphanie, vorteile, dringlichkeit, einwaende, szenario, bonus, faq, letzte_chance, custom
is_active boolean optional Aktiv (ausgesetzte Tage werden übersprungen).
attachment_ids array<integer> optional Anhänge.
include_default_attachments boolean optional Standard-Anhänge.
after_step_id integer optional Nur beim Anlegen: hinter diesem Tag einfügen (Standard: ans Ende).

Antwort

200 OK – ein Tages-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

PATCH /sequences/{id}/steps/{stepId}
curl -X PATCH https://persolz.com/api/v1/sequences/42/steps/31 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Neuer Betreff für Tag 1",
    "is_active": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/steps/31');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'subject' => 'Neuer Betreff für Tag 1',
        'is_active' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42/steps/31', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    subject: 'Neuer Betreff für Tag 1',
    is_active: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/sequences/42/steps/31",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "subject": "Neuer Betreff für Tag 1",
        "is_active": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 31,
    "sequence_id": 7,
    "position": 1,
    "day": 1,
    "is_active": true,
    "stage": "buehne",
    "stage_label": "Bühne bereiten",
    "subject": "Wie alles mit einer Postkarte begann",
    "preheader": "Tag 1 von 5",
    "html": "<!doctype html><html>…</html>",
    "attachment_ids": [],
    "include_default_attachments": false,
    "campaign_id": 950,
    "ab_test_id": null,
    "stats": {
      "sent": 1157,
      "opens": 760,
      "clicks": 98,
      "unsubs": 4,
      "open_rate": 0.6569,
      "click_rate": 0.0847
    },
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-09-02T11:00:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Tag löschen

DELETE /api/v1/sequences/{id}/steps/{stepId}
Scope write Tarif: soap

Löscht den Tag samt Statistik; Teilnehmer behalten ihren Fortschritt.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource
stepId integer Pflicht ID des Tages

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /sequences/{id}/steps/{stepId}
curl -X DELETE https://persolz.com/api/v1/sequences/42/steps/31 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/steps/31');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/sequences/42/steps/31', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/sequences/42/steps/31",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Kontakte aufnehmen

POST /api/v1/sequences/{id}/enroll
Scope write Tarif: soap Idempotency-Key

Nimmt Kontakte auf: per contact_ids, emails (unbekannte Adressen werden als Kontakte angelegt) oder alle aktiven Mitglieder einer Liste (list_id). Abgemeldete/unbestätigte Kontakte werden übersprungen. Die Mails starten, sobald die Sequenz aktiv ist.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
contact_ids array<integer> optional Kontakt-IDs.
emails array<string> optional E-Mail-Adressen.
list_id integer optional Alle aktiven Mitglieder dieser Liste.
start_date string optional Startdatum YYYY-MM-DD, „today“ oder „tomorrow“ (Standard heute). Vergangene Daten holen verpasste Tage sofort nach.

Antwort

200 OK

Antwortfelder
FeldBeschreibung
enrolled integer Neu aufgenommene Kontakte.
already integer Bereits teilnehmende Kontakte (übersprungen).
skipped integer Übersprungene Kontakte (abgemeldet oder unbestätigt).
created integer Neu angelegte Kontakte (unbekannte E-Mail-Adressen).
total integer Teilnehmer insgesamt nach dem Aufnehmen.
message string Verständliche Meldung auf Deutsch.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /sequences/{id}/enroll
curl -X POST https://persolz.com/api/v1/sequences/42/enroll \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "contact_ids": [42],
    "emails": ["ben@beispiel.de"],
    "start_date": "tomorrow"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/enroll');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'contact_ids' => [42],
        'emails' => ['ben@beispiel.de'],
        'start_date' => 'tomorrow',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42/enroll', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    contact_ids: [42],
    emails: ['ben@beispiel.de'],
    start_date: 'tomorrow'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/sequences/42/enroll",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "contact_ids": [42],
        "emails": ["ben@beispiel.de"],
        "start_date": "tomorrow",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "enrolled": 2,
    "already": 0,
    "skipped": 0,
    "created": 1,
    "total": 2,
    "message": "2 Kontakte aufgenommen. 1 neuer Kontakt wurde angelegt."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Teilnehmer auflisten

GET /api/v1/sequences/{id}/enrollments
Scope read Tarif: soap paginiert

Teilnahmen mit Fortschritt (aktueller Tag, nächster Versand).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
status string optional active | paused | completed | cancelled | unsubscribed Werte: active, paused, completed, cancelled, unsubscribed Beispiel: active
q string optional Suche in Name/E-Mail. Beispiel: anna
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Teilnahme-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /sequences/{id}/enrollments
curl "https://persolz.com/api/v1/sequences/42/enrollments?status=active" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'active',
]);
$ch = curl_init('https://persolz.com/api/v1/sequences/42/enrollments?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'active'
});
const response = await fetch('https://persolz.com/api/v1/sequences/42/enrollments?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/sequences/42/enrollments",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "active",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 501,
      "contact_id": 42,
      "email": "anna@beispiel.de",
      "name": "Anna Schmidt",
      "status": "active",
      "status_label": "Aktiv",
      "source": "api",
      "start_date": "2026-10-08",
      "current_position": 2,
      "current_day": 2,
      "total_steps": 5,
      "progress": 0.4,
      "next_send_at": "2026-10-10T09:00:00+02:00",
      "last_sent_at": "2026-10-09T09:00:12+02:00",
      "created_at": "2026-10-08T12:00:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Teilnehmer entfernen

DELETE /api/v1/sequences/{id}/enrollments/{contactId}
Scope write Tarif: soap

Stoppt die Teilnahme des Kontakts (er erhält keine weiteren Mails dieser Sequenz).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource
contactId integer Pflicht ID des Kontakts

Antwort

204 Entfernt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /sequences/{id}/enrollments/{contactId}
curl -X DELETE https://persolz.com/api/v1/sequences/42/enrollments/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/enrollments/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/sequences/42/enrollments/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/sequences/42/enrollments/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Seifenoper aktivieren

POST /api/v1/sequences/{id}/activate
Scope send Tarif: soap Idempotency-Key

Startet die Sequenz. Voraussetzung: mindestens ein aktiver Tag mit Betreff und Inhalt, ein SMTP-Konto und bei list_join eine Liste.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Seifenopern-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /sequences/{id}/activate
curl -X POST https://persolz.com/api/v1/sequences/42/activate \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/activate');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42/activate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Idempotency-Key': crypto.randomUUID()
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/sequences/42/activate",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "status": "active",
    "status_label": "Aktiv",
    "id": 7,
    "name": "Willkommen bei Kartenherz",
    "description": "5 Tage Story-Sequenz für neue Leads",
    "goal": "Verkauf des Kartenset-Abos",
    "trigger_type": "list_join",
    "trigger_list_id": 3,
    "trigger_label": "Beim Eintrag in „Kartenherz Funnel“",
    "send_time": "09:00",
    "skip_weekends": false,
    "smtp_account_id": null,
    "ai_model": "sonnet",
    "template_key": "soap-01",
    "include_default_attachments": false,
    "step_count": 5,
    "enrolled_active": 312,
    "enrolled_completed": 845,
    "sent": 4980,
    "open_rate": 0.61,
    "click_rate": 0.14,
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Seifenoper pausieren

POST /api/v1/sequences/{id}/pause
Scope write Tarif: soap Idempotency-Key

Pausiert die Sequenz – es gehen keine Mails mehr raus, bis sie wieder aktiviert wird.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Seifenopern-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

POST /sequences/{id}/pause
curl -X POST https://persolz.com/api/v1/sequences/42/pause \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/sequences/42/pause');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/sequences/42/pause', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/sequences/42/pause",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "status": "paused",
    "status_label": "Pausiert",
    "id": 7,
    "name": "Willkommen bei Kartenherz",
    "description": "5 Tage Story-Sequenz für neue Leads",
    "goal": "Verkauf des Kartenset-Abos",
    "trigger_type": "list_join",
    "trigger_list_id": 3,
    "trigger_label": "Beim Eintrag in „Kartenherz Funnel“",
    "send_time": "09:00",
    "skip_weekends": false,
    "smtp_account_id": null,
    "ai_model": "sonnet",
    "template_key": "soap-01",
    "include_default_attachments": false,
    "step_count": 5,
    "enrolled_active": 312,
    "enrolled_completed": 845,
    "sent": 4980,
    "open_rate": 0.61,
    "click_rate": 0.14,
    "created_at": "2026-09-01T10:00:00+02:00",
    "updated_at": "2026-10-01T08:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Vorlagen

Vorlagen-Bibliothek und eigene E-Mail-Vorlagen.

Objekte: Vorlagen-Objekt

Das Vorlagen-Objekt

Attribute

Das Vorlagen-Objekt
FeldBeschreibung
source string library (Vorlagen-Bibliothek) oder own (eigene Vorlage).
id integer · null ID der eigenen Vorlage oder null (Bibliothek).
key string Schlüssel der Bibliotheks-Vorlage (bei eigenen: die Ursprungsvorlage).
ref string Referenz für template-Felder: lib:<Schlüssel> oder own:<ID>.
name string Name.
category string Kategorie: seinfeld, soap, email, newsletter oder general.
category_label string Kategorie in Worten.
description string Beschreibung.
tags array<string> Tags (Schlagworte).
subject string Betreff.
preheader string Vorschautext (erscheint im Postfach neben dem Betreff).
theme string Farbschema der Vorlage.
is_favorite boolean Als Favorit markiert.
updated_at string · null Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Vorlagen-Objekt JSON
{
  "source": "library",
  "id": null,
  "key": "newsletter-01",
  "ref": "lib:newsletter-01",
  "name": "Wochen-Digest",
  "category": "newsletter",
  "category_label": "Newsletter",
  "description": "Drei Themen der Woche mit Bild und Button",
  "tags": ["digest"],
  "subject": "Deine Woche in 3 Minuten",
  "preheader": "Das Wichtigste auf einen Blick",
  "theme": "persolz",
  "is_favorite": false,
  "updated_at": null
}

Vorlagen auflisten

GET /api/v1/templates
Scope read paginiert

Bibliothek (über 100 Vorlagen) und eigene Vorlagen, ohne HTML. Eigene Favoriten zuerst.

Query-Parameter

NameBeschreibung
category string optional seinfeld | soap | email | newsletter | general | own Werte: seinfeld, soap, email, newsletter, general, own Beispiel: newsletter
source string optional library | own Werte: library, own Beispiel: library
q string optional Suche. Beispiel: digest
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Vorlagen-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /templates
curl "https://persolz.com/api/v1/templates?category=newsletter" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'category' => 'newsletter',
]);
$ch = curl_init('https://persolz.com/api/v1/templates?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  category: 'newsletter'
});
const response = await fetch('https://persolz.com/api/v1/templates?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/templates",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "category": "newsletter",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "source": "library",
      "id": null,
      "key": "newsletter-01",
      "ref": "lib:newsletter-01",
      "name": "Wochen-Digest",
      "category": "newsletter",
      "category_label": "Newsletter",
      "description": "Drei Themen der Woche mit Bild und Button",
      "tags": ["digest"],
      "subject": "Deine Woche in 3 Minuten",
      "preheader": "Das Wichtigste auf einen Blick",
      "theme": "persolz",
      "is_favorite": false,
      "updated_at": null
    },
    {
      "source": "own",
      "id": 14,
      "key": "newsletter-01",
      "ref": "own:14",
      "name": "Mein Herbst-Newsletter",
      "category": "newsletter",
      "category_label": "Newsletter",
      "description": "Basierend auf „Wochen-Digest“",
      "tags": [],
      "subject": "Herbst bei Kartenherz",
      "preheader": "Neue Karten sind da",
      "theme": "sunrise",
      "is_favorite": true,
      "updated_at": "2026-10-02T12:00:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 2,
      "pages": 1
    }
  }
}

Eigene Vorlage anlegen

POST /api/v1/templates
Scope write Idempotency-Key

Speichert eine eigene Vorlage (oder kopiert eine Bibliotheks-Vorlage mit from_template). Free-Tarif: max. 3 eigene Vorlagen.

Request-Body application/json

FeldBeschreibung
name string Pflicht Name.
category string optional seinfeld | soap | email | newsletter | general Werte: seinfeld, soap, email, newsletter, general
subject string optional Betreff.
preheader string optional Vorschautext.
html string optional HTML (Pflicht, außer from_template).
from_template string optional Kopie einer Bibliotheks-Vorlage (Schlüssel, z. B. newsletter-01).
is_favorite boolean optional Als Favorit markieren.

Antwort

201 Angelegt – ein Vorlagen-Objekt, zusätzlich:

Antwortfelder
FeldBeschreibung
html string HTML-Inhalt.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited

POST /templates
curl -X POST https://persolz.com/api/v1/templates \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mein Herbst-Newsletter",
    "from_template": "newsletter-01"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/templates');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'name' => 'Mein Herbst-Newsletter',
        'from_template' => 'newsletter-01',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/templates', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Mein Herbst-Newsletter',
    from_template: 'newsletter-01'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/templates",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "name": "Mein Herbst-Newsletter",
        "from_template": "newsletter-01",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "source": "own",
    "id": 14,
    "key": "newsletter-01",
    "ref": "own:14",
    "name": "Mein Herbst-Newsletter",
    "category": "newsletter",
    "category_label": "Newsletter",
    "description": "Basierend auf „Wochen-Digest“",
    "tags": [],
    "subject": "Herbst bei Kartenherz",
    "preheader": "Neue Karten sind da",
    "theme": "sunrise",
    "is_favorite": true,
    "updated_at": "2026-10-02T12:00:00+02:00",
    "html": "<!doctype html><html>…</html>"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Vorlage abrufen

GET /api/v1/templates/{key}
Scope read

Eine Vorlage inkl. HTML: Bibliothek per Schlüssel (z. B. seinfeld-01, auch lib:seinfeld-01), eigene per ID (z. B. 14 oder own:14).

Pfad-Parameter

NameBeschreibung
key string Pflicht Schlüssel der Bibliotheks-Vorlage oder ID der eigenen Vorlage

Antwort

200 OK – ein Vorlagen-Objekt, zusätzlich:

Antwortfelder
FeldBeschreibung
html string HTML-Inhalt.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /templates/{key}
curl https://persolz.com/api/v1/templates/seinfeld-01 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/templates/seinfeld-01');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/templates/seinfeld-01', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/templates/seinfeld-01",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "source": "library",
    "id": null,
    "key": "newsletter-01",
    "ref": "lib:newsletter-01",
    "name": "Wochen-Digest",
    "category": "newsletter",
    "category_label": "Newsletter",
    "description": "Drei Themen der Woche mit Bild und Button",
    "tags": ["digest"],
    "subject": "Deine Woche in 3 Minuten",
    "preheader": "Das Wichtigste auf einen Blick",
    "theme": "persolz",
    "is_favorite": false,
    "updated_at": null,
    "html": "<!doctype html><html>…</html>"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Eigene Vorlage ändern

PATCH /api/v1/templates/{id}
Scope write

Ändert eine eigene Vorlage (Bibliotheks-Vorlagen sind schreibgeschützt).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
name string Pflicht Name.
category string optional seinfeld | soap | email | newsletter | general Werte: seinfeld, soap, email, newsletter, general
subject string optional Betreff.
preheader string optional Vorschautext.
html string optional HTML (Pflicht, außer from_template).
from_template string optional Kopie einer Bibliotheks-Vorlage (Schlüssel, z. B. newsletter-01).
is_favorite boolean optional Als Favorit markieren.

Antwort

200 OK – ein Vorlagen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

PATCH /templates/{id}
curl -X PATCH https://persolz.com/api/v1/templates/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "subject": "Neuer Betreff",
    "is_favorite": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/templates/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'subject' => 'Neuer Betreff',
        'is_favorite' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/templates/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    subject: 'Neuer Betreff',
    is_favorite: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/templates/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "subject": "Neuer Betreff",
        "is_favorite": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "source": "own",
    "id": 14,
    "key": "newsletter-01",
    "ref": "own:14",
    "name": "Mein Herbst-Newsletter",
    "category": "newsletter",
    "category_label": "Newsletter",
    "description": "Basierend auf „Wochen-Digest“",
    "tags": [],
    "subject": "Herbst bei Kartenherz",
    "preheader": "Neue Karten sind da",
    "theme": "sunrise",
    "is_favorite": true,
    "updated_at": "2026-10-02T12:00:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Eigene Vorlage löschen

DELETE /api/v1/templates/{id}
Scope write

Löscht eine eigene Vorlage.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /templates/{id}
curl -X DELETE https://persolz.com/api/v1/templates/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/templates/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/templates/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/templates/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

API-Referenz

Anhänge

Dateien in der Anhang-Cloud hochladen, auflisten und löschen.

Objekte: Anhang-Objekt

Das Anhang-Objekt

Attribute

Das Anhang-Objekt
FeldBeschreibung
id integer Eindeutige ID.
name string Dateiname.
size integer Größe in Bytes.
size_human string Größe, lesbar formatiert.
mime string MIME-Typ.
ext string Dateiendung.
group string Dateigruppe: document, image, audio, archive …
is_default boolean Standard-Anhang (wird bei „Standard-Anhänge mitsenden“ angehängt).
description string Beschreibung.
image_url string Nur bei Bildern (JPG, PNG, GIF, WebP): öffentliche, nicht erratbare Adresse für <img src> in E-Mails (/i/<Token>/<Name>), sonst null.
width integer Bildbreite in Pixeln (nur bei Bildern, sonst null).
height integer Bildhöhe in Pixeln (nur bei Bildern, sonst null).
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
Das Anhang-Objekt JSON
{
  "id": 12,
  "name": "Team-Foto.jpg",
  "size": 248112,
  "size_human": "242,3 KB",
  "mime": "image/jpeg",
  "ext": "jpg",
  "group": "image",
  "is_default": false,
  "description": "Unser Team im Sommer",
  "image_url": "https://persolz.com/i/3f9a2c7d1e5b4a60b8c9d0e1f2a3b4c5/team-foto.jpg",
  "width": 1200,
  "height": 800,
  "created_at": "2026-10-02T11:30:00+02:00"
}

Anhänge auflisten

GET /api/v1/attachments
Scope read Tarif: attachments paginiert

Dateien der Anhang-Cloud mit Speicherverbrauch. Bilder (JPG, PNG, GIF, WebP) haben zusätzlich image_url (öffentliche, nicht erratbare Adresse für das src-Attribut von Bildern in E-Mails) sowie width/height in Pixeln – bei allen anderen Dateien (auch SVG) sind diese Felder null. Die Liste zeigt nur bereits vorhandene Bild-Links (ein widerrufener Link wird hier nicht neu erzeugt); GET /attachments/{id} erzeugt den Link bei Bedarf.

Query-Parameter

NameBeschreibung
q string optional Suche im Namen/in der Beschreibung. Beispiel: preis
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Anhang-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /attachments
curl https://persolz.com/api/v1/attachments \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/attachments');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/attachments', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/attachments",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 9,
      "name": "Preisliste-2026.pdf",
      "size": 182340,
      "size_human": "178,1 KB",
      "mime": "application/pdf",
      "ext": "pdf",
      "group": "document",
      "is_default": false,
      "description": "Aktuelle Preisliste",
      "image_url": null,
      "width": null,
      "height": null,
      "created_at": "2026-10-01T09:00:00+02:00"
    },
    {
      "id": 12,
      "name": "Team-Foto.jpg",
      "size": 248112,
      "size_human": "242,3 KB",
      "mime": "image/jpeg",
      "ext": "jpg",
      "group": "image",
      "is_default": false,
      "description": "Unser Team im Sommer",
      "image_url": "https://persolz.com/i/3f9a2c7d1e5b4a60b8c9d0e1f2a3b4c5/team-foto.jpg",
      "width": 1200,
      "height": 800,
      "created_at": "2026-10-02T11:30:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 2,
      "pages": 1
    }
  }
}

Anhang hochladen

POST /api/v1/attachments
Scope write Tarif: attachments Idempotency-Key

Upload als multipart/form-data (Feld „file“) oder JSON {name, content_base64}. Erlaubt sind u. a. PDF, Bilder, Office-Dateien, TXT/CSV, ICS, ZIP, MP3 (max. 10 MB je Datei). Ausführbare Dateien werden abgelehnt. Bei Bildern (JPG, PNG, GIF, WebP) enthält die Antwort image_url – damit baust Du das Bild direkt ins HTML Deiner E-Mails ein (z. B. bei POST /emails/send).

Request-Body multipart/form-data

FeldBeschreibung
file file optional Die Datei (multipart).
name string optional JSON: Dateiname mit Endung.
content_base64 string optional JSON: Dateiinhalt Base64-kodiert.
description string optional Beschreibung.
is_default boolean optional Standard-Anhang (wird bei „Standard-Anhänge mitsenden“ angehängt).

Antwort

201 Hochgeladen – ein Anhang-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 413 payload_too_large 422 validation_error 429 rate_limited

POST /attachments
curl -X POST https://persolz.com/api/v1/attachments \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -F "file=@Preisliste-2026.pdf" \
  -F "description=Aktuelle Preisliste"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/attachments');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_POSTFIELDS     => [
        'file' => new CURLFile('Preisliste-2026.pdf'),
        'description' => 'Aktuelle Preisliste',
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const form = new FormData();
form.append('file', document.querySelector('input[type=file]').files[0]); // Preisliste-2026.pdf
form.append('description', 'Aktuelle Preisliste');

const response = await fetch('https://persolz.com/api/v1/attachments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  },
  body: form
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/attachments",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    files={"file": open("Preisliste-2026.pdf", "rb")},
    data={
        "description": "Aktuelle Preisliste",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 9,
    "name": "Preisliste-2026.pdf",
    "size": 182340,
    "size_human": "178,1 KB",
    "mime": "application/pdf",
    "ext": "pdf",
    "group": "document",
    "is_default": false,
    "description": "Aktuelle Preisliste",
    "image_url": null,
    "width": null,
    "height": null,
    "created_at": "2026-10-01T09:00:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Anhang abrufen

GET /api/v1/attachments/{id}
Scope read Tarif: attachments

Metadaten eines Anhangs (ohne Dateiinhalt). Bei Bildern mit image_url, width und height – fehlt noch ein öffentlicher Bild-Link (z. B. nach dem Widerrufen), wird hier ein neuer, anderer Link erzeugt.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Anhang-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /attachments/{id}
curl https://persolz.com/api/v1/attachments/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/attachments/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/attachments/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/attachments/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 12,
    "name": "Team-Foto.jpg",
    "size": 248112,
    "size_human": "242,3 KB",
    "mime": "image/jpeg",
    "ext": "jpg",
    "group": "image",
    "is_default": false,
    "description": "Unser Team im Sommer",
    "image_url": "https://persolz.com/i/3f9a2c7d1e5b4a60b8c9d0e1f2a3b4c5/team-foto.jpg",
    "width": 1200,
    "height": 800,
    "created_at": "2026-10-02T11:30:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Anhang löschen

DELETE /api/v1/attachments/{id}
Scope write Tarif: attachments

Löscht die Datei. In bereits geplanten Mails wird sie übersprungen; ist sie als Bild eingebaut, liefert ihre image_url danach 404.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /attachments/{id}
curl -X DELETE https://persolz.com/api/v1/attachments/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/attachments/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/attachments/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/attachments/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

API-Referenz

SMTP-Server

Verbundene SMTP-Konten (ohne Passwörter) und Verbindungstest.

Objekte: SMTP-Konto-Objekt

Das SMTP-Konto-Objekt

Attribute

Das SMTP-Konto-Objekt
FeldBeschreibung
id integer Eindeutige ID.
label string Bezeichnung des Kontos.
from_name string Absendername.
from_email string Absenderadresse.
reply_to string · null Antwort-Adresse oder null.
host string SMTP-Server.
port integer Port.
encryption string Verschlüsselung: ssl, tls oder none.
username string Benutzername für die Anmeldung.
has_password boolean Ein Passwort ist hinterlegt (es wird nie ausgegeben).
auth boolean Anmeldung am Server ist aktiv.
hourly_limit integer Max. Mails pro Stunde (0 = kein Limit).
daily_limit integer Max. Mails pro Tag (0 = kein Limit).
is_default boolean Standardkonto für den Versand.
status string ok, error oder untested.
last_error string · null Letzte Fehlermeldung oder null.
last_tested_at string (Datum/Zeit) Letzter Verbindungstest oder null.
sent_today integer Heute über dieses Konto versendet.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
Das SMTP-Konto-Objekt JSON
{
  "id": 1,
  "label": "Strato Postfach",
  "from_name": "Kartenherz",
  "from_email": "hallo@kartenherz.com",
  "reply_to": null,
  "host": "smtp.strato.de",
  "port": 465,
  "encryption": "ssl",
  "username": "hallo@kartenherz.com",
  "has_password": true,
  "auth": true,
  "hourly_limit": 0,
  "daily_limit": 0,
  "is_default": true,
  "status": "ok",
  "last_error": null,
  "last_tested_at": "2026-10-01T09:00:00+02:00",
  "sent_today": 140,
  "created_at": "2026-09-01T10:00:00+02:00"
}

SMTP-Konten auflisten

GET /api/v1/smtp-accounts
Scope read

Verbundene SMTP-Server – Passwörter werden nie ausgegeben (nur has_password).

Antwort

200 OK – Liste von SMTP-Konto-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /smtp-accounts
curl https://persolz.com/api/v1/smtp-accounts \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/smtp-accounts');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/smtp-accounts', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/smtp-accounts",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 1,
      "label": "Strato Postfach",
      "from_name": "Kartenherz",
      "from_email": "hallo@kartenherz.com",
      "reply_to": null,
      "host": "smtp.strato.de",
      "port": 465,
      "encryption": "ssl",
      "username": "hallo@kartenherz.com",
      "has_password": true,
      "auth": true,
      "hourly_limit": 0,
      "daily_limit": 0,
      "is_default": true,
      "status": "ok",
      "last_error": null,
      "last_tested_at": "2026-10-01T09:00:00+02:00",
      "sent_today": 140,
      "created_at": "2026-09-01T10:00:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Verbindung testen

POST /api/v1/smtp-accounts/{id}/test
Scope write Idempotency-Key

Testet die Verbindung (Anmeldung beim Server). Mit send_to wird zusätzlich eine Testmail verschickt. Bei Fehlern gibt es einen deutschen Lösungshinweis.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
send_to string optional Testmail an diese Adresse (optional).

Antwort

200 OK

Antwortfelder
FeldBeschreibung
ok boolean Verbindung (und ggf. Testmail) erfolgreich.
error string · null Fehlermeldung oder null.
hint string · null Deutscher Lösungshinweis bei Fehlern oder null.
message string Verständliche Meldung auf Deutsch.
tested_at string (Datum/Zeit) Zeitpunkt des Tests.
sent_to string Empfänger der Testmail oder null.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /smtp-accounts/{id}/test
curl -X POST https://persolz.com/api/v1/smtp-accounts/42/test \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "send_to": "du@beispiel.de"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/smtp-accounts/42/test');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'send_to' => 'du@beispiel.de',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/smtp-accounts/42/test', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    send_to: 'du@beispiel.de'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/smtp-accounts/42/test",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "send_to": "du@beispiel.de",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "ok": true,
    "error": null,
    "hint": null,
    "message": "Verbindung erfolgreich.",
    "tested_at": "2026-10-09T14:05:00+02:00",
    "sent_to": "du@beispiel.de"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Kampagnen & Analytics

Alle Versände mit Status und Kennzahlen, Gesamtauswertung.

Kampagnen auflisten

GET /api/v1/campaigns
Scope read paginiert

Alle Versände (Newsletter, E-Mails, Seinfeld-Mails, Seifenopern-Tage) mit Status und Kennzahlen.

Query-Parameter

NameBeschreibung
type string optional email | newsletter | seinfeld | soap Werte: email, newsletter, seinfeld, soap Beispiel: newsletter
status string optional draft | scheduled | queued | sending | sent | paused | cancelled | failed | active Beispiel: sent
q string optional Suche in Name/Betreff. Beispiel: herbst
created_after string optional Angelegt nach. Beispiel: 2026-10-01
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Kampagnen-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /campaigns
curl "https://persolz.com/api/v1/campaigns?type=newsletter&status=sent" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'type' => 'newsletter',
    'status' => 'sent',
]);
$ch = curl_init('https://persolz.com/api/v1/campaigns?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  type: 'newsletter',
  status: 'sent'
});
const response = await fetch('https://persolz.com/api/v1/campaigns?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/campaigns",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "type": "newsletter",
        "status": "sent",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 812,
      "type": "newsletter",
      "type_label": "Newsletter",
      "name": "Herbst-Aktion",
      "subject": "Anna, das hier ist nur für Dich",
      "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
      "status": "sent",
      "status_label": "Gesendet",
      "audience": {
        "all": false,
        "lists": [3],
        "contacts": [],
        "emails": []
      },
      "audience_label": "1 Liste",
      "smtp_account_id": 1,
      "attachment_ids": [],
      "include_default_attachments": false,
      "track_opens": true,
      "track_clicks": true,
      "ab_test_id": null,
      "scheduled_at": null,
      "started_at": "2026-10-09T09:00:00+02:00",
      "finished_at": "2026-10-09T09:04:31+02:00",
      "stats": {
        "recipients": 1250,
        "sent": 1248,
        "failed": 2,
        "unique_opens": 702,
        "unique_clicks": 133,
        "unsubs": 3,
        "open_rate": 0.5625,
        "click_rate": 0.1066
      },
      "created_at": "2026-10-08T17:20:00+02:00",
      "updated_at": "2026-10-09T09:04:31+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Kampagne abrufen

GET /api/v1/campaigns/{id}
Scope read

Eine Kampagne mit Kennzahlen (HTML mit ?include=html).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
include string optional html = HTML-Inhalt mitliefern Beispiel: html

Antwort

200 OK – ein Kampagnen-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /campaigns/{id}
curl https://persolz.com/api/v1/campaigns/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/campaigns/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/campaigns/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/campaigns/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "type": "newsletter",
    "type_label": "Newsletter",
    "name": "Herbst-Aktion",
    "subject": "Anna, das hier ist nur für Dich",
    "preheader": "Nur bis Sonntag: Dein Herbst-Bonus",
    "status": "sent",
    "status_label": "Gesendet",
    "audience": {
      "all": false,
      "lists": [3],
      "contacts": [],
      "emails": []
    },
    "audience_label": "1 Liste",
    "smtp_account_id": 1,
    "attachment_ids": [],
    "include_default_attachments": false,
    "track_opens": true,
    "track_clicks": true,
    "ab_test_id": null,
    "scheduled_at": null,
    "started_at": "2026-10-09T09:00:00+02:00",
    "finished_at": "2026-10-09T09:04:31+02:00",
    "stats": {
      "recipients": 1250,
      "sent": 1248,
      "failed": 2,
      "unique_opens": 702,
      "unique_clicks": 133,
      "unsubs": 3,
      "open_rate": 0.5625,
      "click_rate": 0.1066
    },
    "created_at": "2026-10-08T17:20:00+02:00",
    "updated_at": "2026-10-09T09:04:31+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Kampagnen-Statistik

GET /api/v1/campaigns/{id}/stats
Scope read

Detaillierte Zahlen und Quoten einer Kampagne, Top-Links und Geräteverteilung.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK

Alle 32 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
id integer ID der Kampagne.
type string Versandart: email, newsletter, seinfeld oder soap.
status string Status der Kampagne.
stats object Kennzahlen.
stats.recipients integer Empfänger gesamt.
stats.sent integer Versendet.
stats.failed integer Fehlgeschlagen.
stats.pending integer Noch nicht versendet.
stats.held integer Wartet auf den Gewinner eines A/B-Tests.
stats.skipped integer Übersprungen (z. B. inzwischen abgemeldet).
stats.cancelled integer Abgebrochen.
stats.opens integer Öffnungen gesamt (inkl. mehrfacher Öffnungen).
stats.unique_opens integer Empfänger mit mindestens einer Öffnung.
stats.clicks integer Klicks gesamt.
stats.unique_clicks integer Empfänger mit mindestens einem Klick.
stats.unsubs integer Abmeldungen.
stats.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
stats.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
stats.ctor number Click-to-Open-Rate von 0 bis 1 (eindeutige Klicks / eindeutige Öffnungen).
stats.unsub_rate number Abmeldequote von 0 bis 1.
stats.bounce_rate number Bounce-Quote von 0 bis 1.
stats.failed_rate number Anteil fehlgeschlagener Zustellungen von 0 bis 1.
stats.progress number Fortschritt des Versands von 0 bis 1.
links array<object> Meistgeklickte Links.
links[].url string Ziel-URL des Links.
links[].label string Linktext.
links[].clicks integer Klicks gesamt.
links[].unique_clicks integer Empfänger, die geklickt haben.
devices object Öffnungen nach Gerät.
devices.desktop integer Öffnungen am Desktop.
devices.mobile integer Öffnungen auf dem Smartphone.
devices.tablet integer Öffnungen auf dem Tablet.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /campaigns/{id}/stats
curl https://persolz.com/api/v1/campaigns/42/stats \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/campaigns/42/stats');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/campaigns/42/stats', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/campaigns/42/stats",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 812,
    "type": "newsletter",
    "status": "sent",
    "stats": {
      "recipients": 1250,
      "sent": 1248,
      "failed": 2,
      "pending": 0,
      "held": 0,
      "skipped": 0,
      "cancelled": 0,
      "opens": 980,
      "unique_opens": 702,
      "clicks": 190,
      "unique_clicks": 133,
      "unsubs": 3,
      "open_rate": 0.5625,
      "click_rate": 0.1066,
      "ctor": 0.1895,
      "unsub_rate": 0.0024,
      "bounce_rate": 0.0016,
      "failed_rate": 0.0016,
      "progress": 1.0
    },
    "links": [
      {
        "url": "https://kartenherz.com/angebot",
        "label": "Jetzt ansehen",
        "clicks": 140,
        "unique_clicks": 118
      }
    ],
    "devices": {
      "desktop": 312,
      "mobile": 365,
      "tablet": 25
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Analytics-Übersicht

GET /api/v1/analytics/overview
Scope read

Gesamtauswertung für einen Zeitraum: KPIs mit Vorperioden-Vergleich, Verlauf, Module, Geräte, Top-Links und Listenwachstum. Bots werden ausgeschlossen (bots=true schließt sie ein).

Query-Parameter

NameBeschreibung
from string optional Start YYYY-MM-DD (Standard: vor 29 Tagen). Beispiel: 2026-09-10
to string optional Ende YYYY-MM-DD (Standard: heute). Beispiel: 2026-10-09
type string optional all | seinfeld | soap | newsletter | email Werte: all, seinfeld, soap, newsletter, email Beispiel: all
bots boolean optional Bot-Öffnungen/-Klicks einbeziehen. Beispiel: false

Antwort

200 OK

Alle 20 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
period object Ausgewerteter Zeitraum.
period.from string (Datum) Beginn des Zeitraums.
period.to string (Datum) Ende des Zeitraums.
period.days integer Anzahl Tage.
period.type string Gefilterter Versandtyp.
period.label string Zeitraum in Worten.
kpis object Kennzahlen des Zeitraums.
kpis.sent integer Versendete E-Mails.
kpis.unique_opens integer Empfänger mit mindestens einer Öffnung.
kpis.unique_clicks integer Empfänger mit mindestens einem Klick.
kpis.unsubs integer Abmeldungen.
kpis.open_rate number Öffnungsrate von 0 bis 1 (eindeutige Öffnungen / versendet).
kpis.click_rate number Klickrate von 0 bis 1 (eindeutige Klicks / versendet).
timeline object Verlauf je Tag (für Diagramme).
timeline.labels array<string> Beschriftungen der Zeitachse.
timeline.series array Datenreihen.
modules array Kennzahlen je Modul (Seinfeld, Seifenopern, Newsletter, E-Mails).
devices array Öffnungen nach Gerät.
links array Meistgeklickte Links.
growth array Listenwachstum im Zeitraum.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /analytics/overview
curl "https://persolz.com/api/v1/analytics/overview?from=2026-09-10&to=2026-10-09" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'from' => '2026-09-10',
    'to' => '2026-10-09',
]);
$ch = curl_init('https://persolz.com/api/v1/analytics/overview?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  from: '2026-09-10',
  to: '2026-10-09'
});
const response = await fetch('https://persolz.com/api/v1/analytics/overview?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/analytics/overview",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "from": "2026-09-10",
        "to": "2026-10-09",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "period": {
      "from": "2026-09-10",
      "to": "2026-10-09",
      "days": 30,
      "type": "all",
      "label": "letzte 30 Tage (10.09.–09.10.2026)"
    },
    "kpis": {
      "sent": 8420,
      "unique_opens": 4630,
      "unique_clicks": 912,
      "unsubs": 21,
      "open_rate": 0.55,
      "click_rate": 0.108
    },
    "timeline": {
      "labels": ["10.09.", "11.09."],
      "series": []
    },
    "modules": [],
    "devices": [],
    "links": [],
    "growth": []
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

A/B-Tests

A/B-Tests anlegen, Ergebnisse lesen und den Gewinner festlegen.

Objekte: A/B-Test-Objekt

Das A/B-Test-Objekt

Attribute

Das A/B-Test-Objekt
FeldBeschreibung
id integer Eindeutige ID.
name string Name.
target_type string Getestete Mail: newsletter, email, seinfeld oder soap.
target_id integer ID der getesteten Mail.
test_type string Was getestet wird: subject, content, design, sender oder combo.
mode string split (fest aufteilen) oder winner (Testgruppe, danach Gewinner an alle).
split_a integer Anteil von Variante A in Prozent.
sample_pct integer Größe der Testgruppe in Prozent (bei winner).
wait_hours integer Wartezeit bis zur Entscheidung in Stunden.
winner_metric string Entscheidungsmetrik: clicks, opens oder ctr.
status string draft (bereit), running (läuft), completed (entschieden) oder stopped.
status_label string Status in Worten (Deutsch).
winner string · null Gewinner-Variante (A oder B) oder null.
auto_apply_winner boolean Der Gewinner wird automatisch an die übrigen Empfänger versendet.
variants array<object> Die Varianten.
variants[].variant string Variante (A oder B).
variants[].subject string Betreff der Variante.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das A/B-Test-Objekt JSON
{
  "id": 21,
  "name": "Betreff-Test: Herbst-Aktion",
  "target_type": "newsletter",
  "target_id": 812,
  "test_type": "subject",
  "mode": "winner",
  "split_a": 50,
  "sample_pct": 20,
  "wait_hours": 4,
  "winner_metric": "clicks",
  "status": "running",
  "status_label": "Läuft",
  "winner": null,
  "auto_apply_winner": true,
  "variants": [
    {
      "variant": "A",
      "subject": "Anna, das hier ist nur für Dich"
    },
    {
      "variant": "B",
      "subject": "Dein Herbst-Bonus wartet"
    }
  ],
  "created_at": "2026-10-08T17:30:00+02:00",
  "updated_at": "2026-10-09T09:00:00+02:00"
}

A/B-Tests auflisten

GET /api/v1/ab-tests
Scope read Tarif: ab_tests paginiert

Alle A/B-Tests, aktive zuerst.

Query-Parameter

NameBeschreibung
status string optional all | active | draft | running | completed | stopped Beispiel: active
type string optional newsletter | email | seinfeld | soap Beispiel: newsletter
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von A/B-Test-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /ab-tests
curl "https://persolz.com/api/v1/ab-tests?status=active" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'active',
]);
$ch = curl_init('https://persolz.com/api/v1/ab-tests?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'active'
});
const response = await fetch('https://persolz.com/api/v1/ab-tests?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/ab-tests",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "active",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 21,
      "name": "Betreff-Test: Herbst-Aktion",
      "target_type": "newsletter",
      "target_id": 812,
      "test_type": "subject",
      "mode": "winner",
      "split_a": 50,
      "sample_pct": 20,
      "wait_hours": 4,
      "winner_metric": "clicks",
      "status": "running",
      "status_label": "Läuft",
      "winner": null,
      "auto_apply_winner": true,
      "variants": [
        {
          "variant": "A",
          "subject": "Anna, das hier ist nur für Dich"
        },
        {
          "variant": "B",
          "subject": "Dein Herbst-Bonus wartet"
        }
      ],
      "created_at": "2026-10-08T17:30:00+02:00",
      "updated_at": "2026-10-09T09:00:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

A/B-Test anlegen

POST /api/v1/ab-tests
Scope write Tarif: ab_tests Idempotency-Key

Legt einen Test für eine noch nicht versendete Mail an. Variante A = aktueller Inhalt, Variante B = Deine Änderungen (b_subject, b_preheader, b_html, b_from_name). Der Test startet mit dem Versand der Mail.

Request-Body application/json

FeldBeschreibung
target_type string Pflicht newsletter | email | seinfeld | soap (Pflicht, außer bei campaign_id)
target_id integer Pflicht ID der Kampagne (newsletter/email), Seinfeld-Mail bzw. Seifenopern-Tag (Pflicht, außer bei campaign_id).
campaign_id integer optional Kurzform für Newsletter und E-Mails: statt target_type + target_id nur die Kampagnen-ID.
test_type string Pflicht subject | content | design | sender | combo
name string optional Name.
mode string optional split (Aufteilen) | winner (Testgruppe, dann Gewinner an alle)
split_a integer optional Anteil Variante A in % (10–90).
sample_pct integer optional Bei winner: Testgruppe in % (5–50).
wait_hours integer optional Bei winner: Wartezeit bis zur Entscheidung (1–72 Std.).
winner_metric string optional clicks | opens | ctr
auto_apply_winner boolean optional Gewinner automatisch übernehmen.
b_subject string optional Betreff der Variante B.
b_preheader string optional Preheader B.
b_html string optional Inhalt B.
b_from_name string optional Absendername B.

Antwort

201 Angelegt – ein A/B-Test-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 409 conflict 422 validation_error 429 rate_limited

POST /ab-tests
curl -X POST https://persolz.com/api/v1/ab-tests \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "target_type": "newsletter",
    "target_id": 812,
    "test_type": "subject",
    "mode": "winner",
    "b_subject": "Dein Herbst-Bonus wartet"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/ab-tests');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'target_type' => 'newsletter',
        'target_id' => 812,
        'test_type' => 'subject',
        'mode' => 'winner',
        'b_subject' => 'Dein Herbst-Bonus wartet',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/ab-tests', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    target_type: 'newsletter',
    target_id: 812,
    test_type: 'subject',
    mode: 'winner',
    b_subject: 'Dein Herbst-Bonus wartet'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/ab-tests",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "target_type": "newsletter",
        "target_id": 812,
        "test_type": "subject",
        "mode": "winner",
        "b_subject": "Dein Herbst-Bonus wartet",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "status": "draft",
    "status_label": "Bereit",
    "id": 21,
    "name": "Betreff-Test: Herbst-Aktion",
    "target_type": "newsletter",
    "target_id": 812,
    "test_type": "subject",
    "mode": "winner",
    "split_a": 50,
    "sample_pct": 20,
    "wait_hours": 4,
    "winner_metric": "clicks",
    "winner": null,
    "auto_apply_winner": true,
    "variants": [
      {
        "variant": "A",
        "subject": "Anna, das hier ist nur für Dich"
      },
      {
        "variant": "B",
        "subject": "Dein Herbst-Bonus wartet"
      }
    ],
    "created_at": "2026-10-08T17:30:00+02:00",
    "updated_at": "2026-10-09T09:00:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

A/B-Test abrufen

GET /api/v1/ab-tests/{id}
Scope read Tarif: ab_tests

Test mit Varianten und Ergebnissen (Versand, Öffnungen, Klicks, Signifikanz).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein A/B-Test-Objekt, zusätzlich:

Alle 13 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
results object Ergebnisse des Tests.
results.metric string Entscheidungsmetrik.
results.variants object Zahlen je Variante.
results.variants.A object Zahlen dieser Variante.
results.variants.A.sent integer Versendet.
results.variants.A.unique_opens integer Empfänger mit mindestens einer Öffnung.
results.variants.A.unique_clicks integer Empfänger mit mindestens einem Klick.
results.variants.B object Zahlen dieser Variante.
results.variants.B.sent integer Versendet.
results.variants.B.unique_opens integer Empfänger mit mindestens einer Öffnung.
results.variants.B.unique_clicks integer Empfänger mit mindestens einem Klick.
results.confidence number Statistische Sicherheit des Vorsprungs von 0 bis 1.
results.leader string Aktuell führende Variante.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /ab-tests/{id}
curl https://persolz.com/api/v1/ab-tests/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/ab-tests/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/ab-tests/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/ab-tests/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 21,
    "name": "Betreff-Test: Herbst-Aktion",
    "target_type": "newsletter",
    "target_id": 812,
    "test_type": "subject",
    "mode": "winner",
    "split_a": 50,
    "sample_pct": 20,
    "wait_hours": 4,
    "winner_metric": "clicks",
    "status": "running",
    "status_label": "Läuft",
    "winner": null,
    "auto_apply_winner": true,
    "variants": [
      {
        "variant": "A",
        "subject": "Anna, das hier ist nur für Dich"
      },
      {
        "variant": "B",
        "subject": "Dein Herbst-Bonus wartet"
      }
    ],
    "created_at": "2026-10-08T17:30:00+02:00",
    "updated_at": "2026-10-09T09:00:00+02:00",
    "results": {
      "metric": "clicks",
      "variants": {
        "A": {
          "sent": 125,
          "unique_opens": 70,
          "unique_clicks": 11
        },
        "B": {
          "sent": 125,
          "unique_opens": 78,
          "unique_clicks": 19
        }
      },
      "confidence": 0.91,
      "leader": "B"
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Gewinner festlegen

POST /api/v1/ab-tests/{id}/winner
Scope write Tarif: ab_tests Idempotency-Key

Legt den Gewinner fest (variant A/B) oder lässt ihn aus den Daten ermitteln (variant=auto). Wartende Empfänger erhalten danach den Gewinner. Möglich, sobald der Test läuft (also nach dem Versand der Mail).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
variant string optional A | B | auto Werte: A, B, auto

Antwort

200 OK

Antwortfelder
FeldBeschreibung
winner string Festgelegte Gewinner-Variante.
released integer Wartende Empfänger, die jetzt den Gewinner bekommen.
copied boolean Der Inhalt des Gewinners wurde in die Mail übernommen.
message string Verständliche Meldung auf Deutsch.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

POST /ab-tests/{id}/winner
curl -X POST https://persolz.com/api/v1/ab-tests/42/winner \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "variant": "B"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/ab-tests/42/winner');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'variant' => 'B',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/ab-tests/42/winner', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    variant: 'B'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/ab-tests/42/winner",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "variant": "B",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "winner": "B",
    "released": 1000,
    "copied": false,
    "message": "Variante B ist der Gewinner. 1.000 wartende Empfänger bekommen jetzt Variante B."
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Termine

Terminbuch: Termine anlegen, ändern und abfragen.

Objekte: Termin-Objekt

Das Termin-Objekt

Attribute

Das Termin-Objekt
FeldBeschreibung
id integer Eindeutige ID.
title string Titel des Termins.
description string Beschreibung.
location string Ort oder Link (z. B. Zoom).
start_at string (Datum/Zeit) Beginn (ISO 8601).
end_at string (Datum/Zeit) Ende (ISO 8601).
all_day boolean Ganztägiger Termin.
contact_id integer Verknüpfter Kontakt oder null.
contact object Kurzinfo zum verknüpften Kontakt oder null.
contact.id integer ID des Kontakts.
contact.name string Name des Kontakts.
contact.email string E-Mail-Adresse.
contact.phone string Telefonnummer für den Termin.
attendees array<string> E-Mail-Adressen der Teilnehmer.
phone string Telefonnummer für den Termin.
color string Farbe im Kalender: violet, coral, mint, gold, blue, pink, green oder lilac.
status string planned (geplant), done (erledigt), cancelled (abgesagt) oder no_show (nicht erschienen).
status_label string Status in Worten (Deutsch).
reminder_minutes integer Erinnerung so viele Minuten vor Beginn (-1 = keine).
remind_attendees boolean Teilnehmer werden ebenfalls erinnert.
source string Herkunft: app, api, google …
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Termin-Objekt JSON
{
  "id": 55,
  "title": "Erstgespräch Anna Schmidt",
  "description": "Kennenlernen per Video",
  "location": "Zoom",
  "start_at": "2026-10-14T10:00:00+02:00",
  "end_at": "2026-10-14T10:30:00+02:00",
  "all_day": false,
  "contact_id": 42,
  "contact": {
    "id": 42,
    "name": "Anna Schmidt",
    "email": "anna@beispiel.de",
    "phone": "+49 30 1234567"
  },
  "attendees": ["anna@beispiel.de"],
  "phone": "",
  "color": "violet",
  "status": "planned",
  "status_label": "Geplant",
  "reminder_minutes": 60,
  "remind_attendees": true,
  "source": "api",
  "created_at": "2026-10-09T14:05:00+02:00",
  "updated_at": "2026-10-09T14:05:00+02:00"
}

Termine abfragen

GET /api/v1/appointments
Scope read Tarif: calendar paginiert

Termine im Zeitraum (Standard: heute bis +30 Tage), sortiert nach Beginn.

Query-Parameter

NameBeschreibung
from string optional Ab (YYYY-MM-DD oder ISO 8601). Beispiel: 2026-10-01
to string optional Bis (max. 1 Jahr Spanne). Beispiel: 2026-10-31
status string optional Nur dieser Status. Beispiel: planned
contact_id integer optional Nur Termine dieses Kontakts. Beispiel: 42
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Termin-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /appointments
curl "https://persolz.com/api/v1/appointments?from=2026-10-01&to=2026-10-31" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'from' => '2026-10-01',
    'to' => '2026-10-31',
]);
$ch = curl_init('https://persolz.com/api/v1/appointments?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  from: '2026-10-01',
  to: '2026-10-31'
});
const response = await fetch('https://persolz.com/api/v1/appointments?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/appointments",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "from": "2026-10-01",
        "to": "2026-10-31",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 55,
      "title": "Erstgespräch Anna Schmidt",
      "description": "Kennenlernen per Video",
      "location": "Zoom",
      "start_at": "2026-10-14T10:00:00+02:00",
      "end_at": "2026-10-14T10:30:00+02:00",
      "all_day": false,
      "contact_id": 42,
      "contact": {
        "id": 42,
        "name": "Anna Schmidt",
        "email": "anna@beispiel.de",
        "phone": "+49 30 1234567"
      },
      "attendees": ["anna@beispiel.de"],
      "phone": "",
      "color": "violet",
      "status": "planned",
      "status_label": "Geplant",
      "reminder_minutes": 60,
      "remind_attendees": true,
      "source": "api",
      "created_at": "2026-10-09T14:05:00+02:00",
      "updated_at": "2026-10-09T14:05:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Termin anlegen

POST /api/v1/appointments
Scope write Tarif: calendar Idempotency-Key

Legt einen Termin an (Webhook appointment.created; Google-Kalender-Sync, falls verbunden).

Request-Body application/json

FeldBeschreibung
title string Pflicht Titel.
start_at string Pflicht Beginn (ISO 8601).
end_at string optional Ende (Standard: +1 Stunde).
all_day boolean optional Ganztägig.
description string optional Beschreibung.
location string optional Ort oder Link.
contact_id integer optional Verknüpfter Kontakt.
attendee_emails array<string> optional Teilnehmer-Adressen.
phone string optional Telefon.
color string optional Farbe: violet, coral, mint, gold, blue, pink, green, lilac. Werte: violet, coral, mint, gold, blue, pink, green, lilac
reminder_minutes integer optional Erinnerung x Minuten vorher (-1 = keine, 0 = zum Beginn, max. 40320).
remind_attendees boolean optional Auch Teilnehmer erinnern.
status string optional planned | done | cancelled | no_show Werte: planned, done, cancelled, no_show

Antwort

201 Angelegt – ein Termin-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 422 validation_error 429 rate_limited

POST /appointments
curl -X POST https://persolz.com/api/v1/appointments \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Erstgespräch Anna Schmidt",
    "start_at": "2026-10-14T10:00:00+02:00",
    "end_at": "2026-10-14T10:30:00+02:00",
    "contact_id": 42,
    "location": "Zoom"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/appointments');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'title' => 'Erstgespräch Anna Schmidt',
        'start_at' => '2026-10-14T10:00:00+02:00',
        'end_at' => '2026-10-14T10:30:00+02:00',
        'contact_id' => 42,
        'location' => 'Zoom',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/appointments', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    title: 'Erstgespräch Anna Schmidt',
    start_at: '2026-10-14T10:00:00+02:00',
    end_at: '2026-10-14T10:30:00+02:00',
    contact_id: 42,
    location: 'Zoom'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/appointments",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "title": "Erstgespräch Anna Schmidt",
        "start_at": "2026-10-14T10:00:00+02:00",
        "end_at": "2026-10-14T10:30:00+02:00",
        "contact_id": 42,
        "location": "Zoom",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 55,
    "title": "Erstgespräch Anna Schmidt",
    "description": "Kennenlernen per Video",
    "location": "Zoom",
    "start_at": "2026-10-14T10:00:00+02:00",
    "end_at": "2026-10-14T10:30:00+02:00",
    "all_day": false,
    "contact_id": 42,
    "contact": {
      "id": 42,
      "name": "Anna Schmidt",
      "email": "anna@beispiel.de",
      "phone": "+49 30 1234567"
    },
    "attendees": ["anna@beispiel.de"],
    "phone": "",
    "color": "violet",
    "status": "planned",
    "status_label": "Geplant",
    "reminder_minutes": 60,
    "remind_attendees": true,
    "source": "api",
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Termin abrufen

GET /api/v1/appointments/{id}
Scope read Tarif: calendar

Ein Termin.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Termin-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /appointments/{id}
curl https://persolz.com/api/v1/appointments/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/appointments/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/appointments/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/appointments/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 55,
    "title": "Erstgespräch Anna Schmidt",
    "description": "Kennenlernen per Video",
    "location": "Zoom",
    "start_at": "2026-10-14T10:00:00+02:00",
    "end_at": "2026-10-14T10:30:00+02:00",
    "all_day": false,
    "contact_id": 42,
    "contact": {
      "id": 42,
      "name": "Anna Schmidt",
      "email": "anna@beispiel.de",
      "phone": "+49 30 1234567"
    },
    "attendees": ["anna@beispiel.de"],
    "phone": "",
    "color": "violet",
    "status": "planned",
    "status_label": "Geplant",
    "reminder_minutes": 60,
    "remind_attendees": true,
    "source": "api",
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Termin ändern

PATCH /api/v1/appointments/{id}
Scope write Tarif: calendar

Ändert nur die übergebenen Felder (Webhook appointment.updated).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
title string Pflicht Titel.
start_at string Pflicht Beginn (ISO 8601).
end_at string optional Ende (Standard: +1 Stunde).
all_day boolean optional Ganztägig.
description string optional Beschreibung.
location string optional Ort oder Link.
contact_id integer optional Verknüpfter Kontakt.
attendee_emails array<string> optional Teilnehmer-Adressen.
phone string optional Telefon.
color string optional Farbe: violet, coral, mint, gold, blue, pink, green, lilac. Werte: violet, coral, mint, gold, blue, pink, green, lilac
reminder_minutes integer optional Erinnerung x Minuten vorher (-1 = keine, 0 = zum Beginn, max. 40320).
remind_attendees boolean optional Auch Teilnehmer erinnern.
status string optional planned | done | cancelled | no_show Werte: planned, done, cancelled, no_show

Antwort

200 OK – ein Termin-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

PATCH /appointments/{id}
curl -X PATCH https://persolz.com/api/v1/appointments/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "start_at": "2026-10-14T11:00:00+02:00",
    "end_at": "2026-10-14T11:30:00+02:00"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/appointments/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'start_at' => '2026-10-14T11:00:00+02:00',
        'end_at' => '2026-10-14T11:30:00+02:00',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/appointments/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    start_at: '2026-10-14T11:00:00+02:00',
    end_at: '2026-10-14T11:30:00+02:00'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/appointments/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "start_at": "2026-10-14T11:00:00+02:00",
        "end_at": "2026-10-14T11:30:00+02:00",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 55,
    "title": "Erstgespräch Anna Schmidt",
    "description": "Kennenlernen per Video",
    "location": "Zoom",
    "start_at": "2026-10-14T10:00:00+02:00",
    "end_at": "2026-10-14T10:30:00+02:00",
    "all_day": false,
    "contact_id": 42,
    "contact": {
      "id": 42,
      "name": "Anna Schmidt",
      "email": "anna@beispiel.de",
      "phone": "+49 30 1234567"
    },
    "attendees": ["anna@beispiel.de"],
    "phone": "",
    "color": "violet",
    "status": "planned",
    "status_label": "Geplant",
    "reminder_minutes": 60,
    "remind_attendees": true,
    "source": "api",
    "created_at": "2026-10-09T14:05:00+02:00",
    "updated_at": "2026-10-09T14:05:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Termin löschen

DELETE /api/v1/appointments/{id}
Scope write Tarif: calendar

Löscht den Termin.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /appointments/{id}
curl -X DELETE https://persolz.com/api/v1/appointments/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/appointments/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/appointments/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/appointments/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

API-Referenz

Posteingang

Verbundene IMAP-Postfächer: Mails lesen und sortieren, antworten, KI-Entwürfe schreiben lassen und freigeben, Kennzahlen abrufen.

Der Posteingang braucht die Tarif-Funktion inbox und ein in der App verbundenes IMAP-Postfach. Die API arbeitet mit dem Zwischenspeicher (neue Mails sofort: POST /mailboxes/{id}/sync); Inhalte gibt es nur einzeln mit include_body=true. Senden braucht den Scope send, KI-Entwürfe den Scope ai und die Funktion ai. Anleitung: Posteingang per API.

Objekte: Postfach-Objekt · Mail-Objekt (Posteingang) · Entwurfs-Objekt (Posteingang)

Das Postfach-Objekt

Ein verbundenes IMAP-Postfach mit Abruf-Status und KI-Einstellungen – das Passwort gibt die API nie heraus.

Attribute

Das Postfach-Objekt
FeldBeschreibung
id integer Eindeutige ID.
label string Bezeichnung des Postfachs in der App.
email string Adresse des Postfachs (Absender der Antworten).
display_name string Absendername.
color string Farbe als Hex-Wert (#RRGGBB).
provider string Anbieter-Vorlage: gmail, outlook, microsoft365, icloud, gmx, webde, tonline, ionos, strato, … oder custom.
provider_name string Anbieter in Worten.
imap_host string IMAP-Server.
imap_port integer IMAP-Port (993 = SSL/TLS, 143 = STARTTLS).
imap_encryption string Verschlüsselung: ssl (SSL/TLS) oder tls (STARTTLS).
imap_username string Benutzername beim IMAP-Server. Das Passwort wird nie ausgegeben.
smtp_account_id integer SMTP-Konto, über das Antworten gesendet werden (GET /smtp), oder null.
sync_enabled boolean Neue Mails werden automatisch abgerufen.
status string new (noch nie abgerufen), ok, syncing (Abruf läuft), error (siehe last_error) oder paused.
status_label string Status in Worten (Deutsch).
last_error string · null Letzter Fehler beim Abruf oder null.
last_error_hint string · null Tipp zur Behebung des letzten Fehlers oder null.
last_sync_at string (Datum/Zeit) Letzter erfolgreicher Abruf oder null.
unread integer Ungelesene Mails im Posteingang.
ai object KI-Einstellungen des Postfachs.
ai.triage boolean Neue Mails werden eingeordnet (Kategorie, Priorität, Zusammenfassung).
ai.mode string off (aus), suggest (Vorschläge auf Knopfdruck), approve (KI schreibt, Du gibst frei) oder auto (Autopilot).
ai.mode_label string KI-Modus in Worten.
ai.model string KI-Modell für Antworten: sonnet oder haiku.
ai.auto_categories array<string> Kategorien, die der Autopilot selbst beantworten darf.
ai.min_confidence integer Mindest-Sicherheit der KI (0–100) für automatische Antworten.
ai.disclosure boolean Automatische Antworten tragen einen KI-Hinweis.
ai.reply_delay_minutes integer Wartezeit in Minuten, bevor eine Autopilot-Antwort rausgeht – so lange kannst Du sie stoppen.
ai.daily_cap integer Max. Autopilot-Antworten pro Tag für dieses Postfach (zusätzlich gilt das Limit Deines Tarifs).
ai.business_hours object Geschäftszeiten für automatische Antworten oder null (= jederzeit).
ai.business_hours.days array<integer> Wochentage (1 = Montag … 7 = Sonntag).
ai.business_hours.from string Beginn (HH:MM).
ai.business_hours.to string Ende (HH:MM).
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
Das Postfach-Objekt JSON
{
  "id": 2,
  "label": "Kartenherz Kundenservice",
  "email": "hallo@kartenherz.com",
  "display_name": "Kartenherz",
  "color": "#5B3DF5",
  "provider": "strato",
  "provider_name": "Strato",
  "imap_host": "imap.strato.de",
  "imap_port": 993,
  "imap_encryption": "ssl",
  "imap_username": "hallo@kartenherz.com",
  "smtp_account_id": 1,
  "sync_enabled": true,
  "status": "ok",
  "status_label": "Verbunden",
  "last_error": null,
  "last_error_hint": null,
  "last_sync_at": "2026-10-09T14:05:00+02:00",
  "unread": 3,
  "ai": {
    "triage": true,
    "mode": "approve",
    "mode_label": "Mit Freigabe – KI schreibt Antworten, Du gibst sie frei",
    "model": "sonnet",
    "auto_categories": ["lead", "customer", "appointment"],
    "min_confidence": 85,
    "disclosure": true,
    "reply_delay_minutes": 15,
    "daily_cap": 20,
    "business_hours": {
      "days": [1, 2, 3, 4, 5],
      "from": "08:00",
      "to": "18:00"
    }
  },
  "created_at": "2026-10-01T09:00:00+02:00"
}

Das Mail-Objekt (Posteingang)

Eine Mail aus dem Zwischenspeicher des Posteingangs mit KI-Einordnung – ohne Inhalt (den liefert GET /inbox/messages/{id}?include_body=true). Auch die Webhooks inbox.* enthalten dieses Objekt.

Attribute

Das Mail-Objekt (Posteingang)
FeldBeschreibung
id integer Eindeutige ID.
mailbox_id integer ID des Postfachs.
mailbox_email string Adresse des Postfachs.
folder string Ordner in Worten (z. B. Posteingang).
folder_role string Rolle des Ordners: inbox, sent, archive, trash, junk … oder null.
thread_key string Kennung der Unterhaltung (gleich für alle Mails eines Verlaufs).
message_id string Message-ID-Header der Mail oder null.
direction string in (empfangen) oder out (von Dir gesendet).
from object Absender.
from.email string E-Mail-Adresse des Absenders.
from.name string Name des Absenders (kann leer sein).
to array<object> Empfänger (Liste aus {email, name}).
to[].email string E-Mail-Adresse.
to[].name string Name (kann leer sein).
cc array Kopie-Empfänger (Liste aus {email, name}).
subject string Betreff.
snippet string Vorschau (Anfang des Textes).
received_at string (Datum/Zeit) Eingang (ISO 8601).
is_seen boolean Gelesen.
is_flagged boolean Markiert (Stern).
is_answered boolean Beantwortet.
has_attachments boolean Hat Anhänge.
has_calendar boolean Enthält eine Kalendereinladung (.ics).
category string KI-Kategorie: lead, customer, appointment, invoice, order, campaign_reply, partner, personal, newsletter, notification, unsubscribe, complaint, spam, other – oder null (noch nicht eingeordnet).
category_label string Kategorie in Worten.
priority string Priorität laut KI: high, normal oder low – oder null.
sentiment string Stimmung laut KI: positive, neutral oder negative – oder null.
intent string Anliegen in wenigen Worten (KI) oder null.
summary string Zusammenfassung in einem Satz (KI) oder null.
needs_reply boolean Die Mail braucht laut KI eine Antwort (null = noch nicht eingeordnet).
contact_id integer Zugeordneter Kontakt (Absender ist in Deinen Kontakten) oder null.
campaign_id integer · null Kampagne, auf die geantwortet wurde (Leser-Antwort), oder null.
labels array<string> Eigene Labels.
snoozed_until string · null Zurückgestellt bis (ISO 8601) oder null.
follow_up_at string · null Erinnerung, falls bis dahin keine Antwort kommt (ISO 8601), oder null.
replied_at string · null Zeitpunkt Deiner Antwort aus Persolz oder null.
has_ai_draft boolean Es gibt einen KI-Entwurf zu dieser Mail (zur Freigabe oder als Vorschlag).
Das Mail-Objekt (Posteingang) JSON
{
  "id": 9120,
  "mailbox_id": 2,
  "mailbox_email": "hallo@kartenherz.com",
  "folder": "Posteingang",
  "folder_role": "inbox",
  "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
  "message_id": "<CAF-anfrage-4711@mail.beispiel.de>",
  "direction": "in",
  "from": {
    "email": "anna@beispiel.de",
    "name": "Anna Schmidt"
  },
  "to": [
    {
      "email": "hallo@kartenherz.com",
      "name": "Kartenherz"
    }
  ],
  "cc": [],
  "subject": "Frage zu Hochzeitskarten",
  "snippet": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung …",
  "received_at": "2026-10-09T14:05:00+02:00",
  "is_seen": false,
  "is_flagged": false,
  "is_answered": false,
  "has_attachments": false,
  "has_calendar": false,
  "category": "lead",
  "category_label": "Leads & Anfragen",
  "priority": "high",
  "sentiment": "positive",
  "intent": "Angebot für 80 Hochzeitskarten",
  "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
  "needs_reply": true,
  "contact_id": 42,
  "campaign_id": null,
  "labels": ["Hochzeit"],
  "snoozed_until": null,
  "follow_up_at": null,
  "replied_at": null,
  "has_ai_draft": true
}

Das Entwurfs-Objekt (Posteingang)

Eine ausgehende Mail: Entwurf, KI-Entwurf zur Freigabe, geplante, gesendete oder fehlgeschlagene Mail.

Attribute

Das Entwurfs-Objekt (Posteingang)
FeldBeschreibung
id integer Eindeutige ID.
mailbox_id integer Absender-Postfach.
kind string reply, reply_all, forward, new oder auto (Autopilot).
kind_label string Art in Worten.
status string draft, pending (wartet auf Freigabe), scheduled (geplant), sending, sent, failed, rejected oder cancelled.
status_label string Status in Worten (Deutsch).
source string user (von Dir), ai (KI-Entwurf) oder rule (Regel).
reply_to_message_id integer Bezugsmail (Antwort/Weiterleitung) oder null.
thread_key string Kennung der Unterhaltung (gleich für alle Mails eines Verlaufs).
to array<object> Empfänger (Liste aus {email, name}).
to[].email string E-Mail-Adresse.
to[].name string Name (kann leer sein).
cc array Kopie-Empfänger (Liste aus {email, name}).
bcc array Blindkopie-Empfänger (Liste aus {email, name}).
subject string Betreff.
html string Inhalt als HTML.
text string Inhalt als Text.
attachment_ids array Anhänge aus der Anhang-Cloud.
mail_attachment_ids array Mitgesendete Anhänge der Originalmail (Weiterleitung).
send_at string (Datum/Zeit) Geplanter Versand (ISO 8601) oder null.
sent_at string (Datum/Zeit) Zeitpunkt des Versands oder null.
sent_message_id string Message-ID der gesendeten Mail oder null.
error string · null Fehler beim Versand oder null.
ai string · null KI-Angaben (nur bei KI-Entwürfen, sonst null).
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string (Datum/Zeit) Zeitpunkt der letzten Änderung (ISO 8601) oder null.
Das Entwurfs-Objekt (Posteingang) JSON
{
  "id": 5502,
  "mailbox_id": 2,
  "kind": "reply",
  "kind_label": "Antwort",
  "status": "sent",
  "status_label": "Gesendet",
  "source": "user",
  "reply_to_message_id": 9120,
  "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
  "to": [
    {
      "email": "anna@beispiel.de",
      "name": "Anna Schmidt"
    }
  ],
  "cc": [],
  "bcc": [],
  "subject": "Re: Frage zu Hochzeitskarten",
  "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
  "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
  "attachment_ids": [],
  "mail_attachment_ids": [],
  "send_at": "2026-10-09T14:21:00+02:00",
  "sent_at": "2026-10-09T14:21:04+02:00",
  "sent_message_id": "<6f1c0d9a2b7e4c1f@kartenherz.com>",
  "error": null,
  "ai": null,
  "created_at": "2026-10-09T14:06:10+02:00",
  "updated_at": "2026-10-09T14:06:10+02:00"
}

Postfächer auflisten

GET /api/v1/mailboxes
Scope read Tarif: inbox

Alle verbundenen IMAP-Postfächer mit Status, letztem Abruf, ungelesenen Mails im Posteingang und den KI-Einstellungen. Passwörter werden nie ausgegeben.

Antwort

200 OK – Liste von Postfach-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /mailboxes
curl https://persolz.com/api/v1/mailboxes \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/mailboxes');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/mailboxes', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/mailboxes",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 2,
      "label": "Kartenherz Kundenservice",
      "email": "hallo@kartenherz.com",
      "display_name": "Kartenherz",
      "color": "#5B3DF5",
      "provider": "strato",
      "provider_name": "Strato",
      "imap_host": "imap.strato.de",
      "imap_port": 993,
      "imap_encryption": "ssl",
      "imap_username": "hallo@kartenherz.com",
      "smtp_account_id": 1,
      "sync_enabled": true,
      "status": "ok",
      "status_label": "Verbunden",
      "last_error": null,
      "last_error_hint": null,
      "last_sync_at": "2026-10-09T14:05:00+02:00",
      "unread": 3,
      "ai": {
        "triage": true,
        "mode": "approve",
        "mode_label": "Mit Freigabe – KI schreibt Antworten, Du gibst sie frei",
        "model": "sonnet",
        "auto_categories": ["lead", "customer", "appointment"],
        "min_confidence": 85,
        "disclosure": true,
        "reply_delay_minutes": 15,
        "daily_cap": 20,
        "business_hours": {
          "days": [1, 2, 3, 4, 5],
          "from": "08:00",
          "to": "18:00"
        }
      },
      "created_at": "2026-10-01T09:00:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Postfach abrufen

GET /api/v1/mailboxes/{id}
Scope read Tarif: inbox

Ein Postfach mit seinen Ordnern (Rolle, Synchronisation, ungelesen/gesamt im Zwischenspeicher).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Postfach-Objekt, zusätzlich:

Antwortfelder
FeldBeschreibung
folders array<object> Ordner des Postfachs.
folders[].id integer Eindeutige ID.
folders[].name string Ordnername auf dem Server.
folders[].display_name string Ordnername in Worten.
folders[].role string Rolle: inbox, sent, drafts, archive, trash, junk, all oder null.
folders[].is_synced boolean Der Ordner wird zwischengespeichert (Liste, Suche, Ansichten).
folders[].unread integer Ungelesene Mails im Zwischenspeicher.
folders[].total integer Mails im Zwischenspeicher.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /mailboxes/{id}
curl https://persolz.com/api/v1/mailboxes/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/mailboxes/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/mailboxes/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/mailboxes/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 2,
    "label": "Kartenherz Kundenservice",
    "email": "hallo@kartenherz.com",
    "display_name": "Kartenherz",
    "color": "#5B3DF5",
    "provider": "strato",
    "provider_name": "Strato",
    "imap_host": "imap.strato.de",
    "imap_port": 993,
    "imap_encryption": "ssl",
    "imap_username": "hallo@kartenherz.com",
    "smtp_account_id": 1,
    "sync_enabled": true,
    "status": "ok",
    "status_label": "Verbunden",
    "last_error": null,
    "last_error_hint": null,
    "last_sync_at": "2026-10-09T14:05:00+02:00",
    "unread": 3,
    "ai": {
      "triage": true,
      "mode": "approve",
      "mode_label": "Mit Freigabe – KI schreibt Antworten, Du gibst sie frei",
      "model": "sonnet",
      "auto_categories": ["lead", "customer", "appointment"],
      "min_confidence": 85,
      "disclosure": true,
      "reply_delay_minutes": 15,
      "daily_cap": 20,
      "business_hours": {
        "days": [1, 2, 3, 4, 5],
        "from": "08:00",
        "to": "18:00"
      }
    },
    "created_at": "2026-10-01T09:00:00+02:00",
    "folders": [
      {
        "id": 11,
        "name": "INBOX",
        "display_name": "Posteingang",
        "role": "inbox",
        "is_synced": true,
        "unread": 3,
        "total": 214
      },
      {
        "id": 12,
        "name": "Gesendet",
        "display_name": "Gesendet",
        "role": "sent",
        "is_synced": true,
        "unread": 0,
        "total": 96
      }
    ]
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Postfach jetzt abrufen

POST /api/v1/mailboxes/{id}/sync
Scope write Tarif: inbox Idempotency-Key

Ruft neue Mails sofort ab (statt auf den Cron zu warten; max. 15 Sekunden, große Postfächer werden in mehreren Läufen vollständig). Ohne KI-Funktion werden neue und noch nicht eingeordnete Mails dabei gleich regelbasiert sortiert (ohne KI-Kosten). Höchstens 8 Abrufe pro Minute.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK

Alle 11 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
mailbox_id integer ID des Postfachs.
ok boolean Abruf erfolgreich.
new integer Neu abgerufene Mails.
updated integer Mails mit geänderten Markierungen (gelesen, markiert …).
removed integer Auf dem Server gelöschte oder verschobene Mails.
partial boolean Zeitbudget erreicht – der Rest folgt beim nächsten Abruf.
busy boolean Ein anderer Abruf läuft gerade.
error string · null Fehlermeldung oder null.
hint string · null Tipp zur Behebung oder null.
duration_ms integer Dauer des Abrufs in Millisekunden.
last_sync_at string (Datum/Zeit) Letzter erfolgreicher Abruf oder null.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 429 rate_limited

POST /mailboxes/{id}/sync
curl -X POST https://persolz.com/api/v1/mailboxes/42/sync \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/mailboxes/42/sync');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/mailboxes/42/sync', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/mailboxes/42/sync",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "mailbox_id": 2,
    "ok": true,
    "new": 4,
    "updated": 1,
    "removed": 0,
    "partial": false,
    "busy": false,
    "error": null,
    "hint": null,
    "duration_ms": 1830,
    "last_sync_at": "2026-10-09T14:05:00+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Mails auflisten

GET /api/v1/inbox/messages
Scope read Tarif: inbox paginiert

Mails aus dem Zwischenspeicher, neueste zuerst – ohne Inhalt (den liefert GET /inbox/messages/{id}?include_body=true). Ansichten wie in der App: focus (braucht eine Antwort, letzte 60 Tage), all (Posteingang), unread, flagged, waiting (Erinnerung gesetzt), snoozed (zurückgestellt), approvals (KI-Entwurf wartet), sent, archive, trash, spam, any (alle Ordner außer Papierkorb/Spam). has_ai_draft = es gibt eine KI-Antwort zur Freigabe oder als Vorschlag.

Query-Parameter

NameBeschreibung
view string optional Ansicht (Standard all). Werte: focus, all, unread, flagged, waiting, snoozed, approvals, sent, archive, trash, spam, any Beispiel: focus
mailbox_id integer optional Nur dieses Postfach. Beispiel: 2
folder_id integer optional Nur dieser Ordner (siehe GET /mailboxes/{id}); ersetzt view.
category string optional KI-Kategorie, z. B. lead, customer, invoice, campaign_reply (Leser-Antworten). Werte: lead, customer, appointment, invoice, order, campaign_reply, partner, personal, newsletter, notification, unsubscribe, complaint, spam, other Beispiel: lead
q string optional Suche in Betreff, Absender, Vorschau und Text. Beispiel: hochzeit
contact_id integer optional Nur Mails dieses Kontakts. Beispiel: 42
since string optional Nur Mails, die ab diesem Zeitpunkt eingegangen sind (ISO 8601 oder YYYY-MM-DD) – ideal zum Abgleichen. Beispiel: 2026-10-09T00:00:00+02:00
label string optional Nur Mails mit diesem Label. Beispiel: Hochzeit
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Mail-Objekt (Posteingang)en

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /inbox/messages
curl "https://persolz.com/api/v1/inbox/messages?view=focus&category=lead" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'view' => 'focus',
    'category' => 'lead',
]);
$ch = curl_init('https://persolz.com/api/v1/inbox/messages?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  view: 'focus',
  category: 'lead'
});
const response = await fetch('https://persolz.com/api/v1/inbox/messages?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/inbox/messages",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "view": "focus",
        "category": "lead",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 9120,
      "mailbox_id": 2,
      "mailbox_email": "hallo@kartenherz.com",
      "folder": "Posteingang",
      "folder_role": "inbox",
      "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
      "message_id": "<CAF-anfrage-4711@mail.beispiel.de>",
      "direction": "in",
      "from": {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      },
      "to": [
        {
          "email": "hallo@kartenherz.com",
          "name": "Kartenherz"
        }
      ],
      "cc": [],
      "subject": "Frage zu Hochzeitskarten",
      "snippet": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung …",
      "received_at": "2026-10-09T14:05:00+02:00",
      "is_seen": false,
      "is_flagged": false,
      "is_answered": false,
      "has_attachments": false,
      "has_calendar": false,
      "category": "lead",
      "category_label": "Leads & Anfragen",
      "priority": "high",
      "sentiment": "positive",
      "intent": "Angebot für 80 Hochzeitskarten",
      "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
      "needs_reply": true,
      "contact_id": 42,
      "campaign_id": null,
      "labels": ["Hochzeit"],
      "snoozed_until": null,
      "follow_up_at": null,
      "replied_at": null,
      "has_ai_draft": true
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Mail abrufen

GET /api/v1/inbox/messages/{id}
Scope read Tarif: inbox

Eine Mail mit Anhängen (Metadaten), KI-Auswertung (Aktionen, Risiken), Verlauf und Entwürfen. Mit include_body=true auch der Inhalt: text und html (bereits bereinigt – externe Bilder sind blockiert, solange images nicht true ist). Ist der Inhalt nicht mehr im Zwischenspeicher, wird er vom Mailserver geladen. Der Abruf markiert die Mail nicht als gelesen (dafür POST /inbox/messages/actions mit action=read).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
include_body boolean optional Inhalt (text + html) mitliefern. Beispiel: true
images boolean optional Externe Bilder im HTML wiederherstellen (Vorsicht: Tracking-Pixel). Beispiel: false

Antwort

200 OK – ein Mail-Objekt (Posteingang), zusätzlich:

Alle 38 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
ai object KI-Einordnung der Mail.
ai.status string Stand der KI-Einordnung: pending, done, skipped oder error.
ai.actions array<object> Vorgeschlagene nächste Schritte.
ai.actions[].key string Schlüssel der Aktion (z. B. reply, create_contact, book_appointment).
ai.actions[].label string Aktion in Worten.
ai.risk_flags array Risiken: legal, money, medical, threat, prompt_injection, personal_data, unclear.
ai.risk_flag_labels array Risiken in Worten.
ai.language string Sprache der Mail (z. B. de, en) oder null.
ai.confidence integer Sicherheit der Einordnung (0–100) oder null.
attachments array<object> Anhänge (nur Metadaten).
attachments[].id integer Eindeutige ID.
attachments[].filename string Dateiname.
attachments[].mime string MIME-Typ.
attachments[].size integer Größe in Bytes.
attachments[].is_inline boolean Eingebettetes Bild (cid:).
attachments[].content_id integer · null Content-ID eingebetteter Bilder oder null.
thread array<object> Verlauf: alle Mails dieser Unterhaltung (ältere zuerst).
thread[].id integer Eindeutige ID.
thread[].direction string in (empfangen) oder out (gesendet).
thread[].subject string Betreff.
thread[].from object Absender.
thread[].from.email string E-Mail-Adresse des Absenders.
thread[].from.name string Name des Absenders (kann leer sein).
thread[].received_at string (Datum/Zeit) Eingang (ISO 8601).
thread[].snippet string Vorschau (Anfang des Textes).
drafts array<object> Entwürfe und ausgehende Mails zu dieser Mail (neueste zuerst).
drafts[].id integer Eindeutige ID.
drafts[].kind string reply, reply_all, forward, new oder auto.
drafts[].status string draft, pending, scheduled, sending, sent, failed, rejected oder cancelled.
drafts[].source string user (von Dir), ai (KI) oder rule (Regel).
drafts[].subject string Betreff.
drafts[].send_at string · null Geplanter Versand oder null.
body object Inhalt (nur mit include_body=true).
body.text string Inhalt als Text.
body.html string Inhalt als bereinigtes HTML (ohne Skripte; externe Bilder blockiert, solange images nicht true ist).
body.has_remote_images boolean Die Mail lädt externe Bilder (Tracking möglich).
body.missing boolean Der Inhalt ist auf dem Server nicht mehr vorhanden.
body.error string · null Fehler beim Nachladen vom Server oder null.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /inbox/messages/{id}
curl "https://persolz.com/api/v1/inbox/messages/42?include_body=true" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'include_body' => 'true',
]);
$ch = curl_init('https://persolz.com/api/v1/inbox/messages/42?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  include_body: 'true'
});
const response = await fetch('https://persolz.com/api/v1/inbox/messages/42?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/inbox/messages/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "include_body": "true",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 9120,
    "mailbox_id": 2,
    "mailbox_email": "hallo@kartenherz.com",
    "folder": "Posteingang",
    "folder_role": "inbox",
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "message_id": "<CAF-anfrage-4711@mail.beispiel.de>",
    "direction": "in",
    "from": {
      "email": "anna@beispiel.de",
      "name": "Anna Schmidt"
    },
    "to": [
      {
        "email": "hallo@kartenherz.com",
        "name": "Kartenherz"
      }
    ],
    "cc": [],
    "subject": "Frage zu Hochzeitskarten",
    "snippet": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung …",
    "received_at": "2026-10-09T14:05:00+02:00",
    "is_seen": false,
    "is_flagged": false,
    "is_answered": false,
    "has_attachments": false,
    "has_calendar": false,
    "category": "lead",
    "category_label": "Leads & Anfragen",
    "priority": "high",
    "sentiment": "positive",
    "intent": "Angebot für 80 Hochzeitskarten",
    "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
    "needs_reply": true,
    "contact_id": 42,
    "campaign_id": null,
    "labels": ["Hochzeit"],
    "snoozed_until": null,
    "follow_up_at": null,
    "replied_at": null,
    "ai": {
      "status": "done",
      "actions": [
        {
          "key": "reply",
          "label": "Antwort entwerfen"
        },
        {
          "key": "create_contact",
          "label": "Kontakt anlegen"
        }
      ],
      "risk_flags": [],
      "risk_flag_labels": [],
      "language": "de",
      "confidence": 92
    },
    "attachments": [
      {
        "id": 301,
        "filename": "Wunschmotiv.jpg",
        "mime": "image/jpeg",
        "size": 184220,
        "is_inline": false,
        "content_id": null
      }
    ],
    "thread": [
      {
        "id": 9120,
        "direction": "in",
        "subject": "Frage zu Hochzeitskarten",
        "from": {
          "email": "anna@beispiel.de",
          "name": "Anna Schmidt"
        },
        "received_at": "2026-10-09T14:05:00+02:00",
        "snippet": "Hallo, wir heiraten im Mai …"
      }
    ],
    "drafts": [
      {
        "id": 5502,
        "kind": "reply",
        "status": "pending",
        "source": "ai",
        "subject": "Re: Frage zu Hochzeitskarten",
        "send_at": null
      }
    ],
    "body": {
      "text": "Hallo,\n\nwir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung …",
      "html": "<p>Hallo,</p><p>wir heiraten im Mai …</p>",
      "has_remote_images": false,
      "missing": false,
      "error": null
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Mails bearbeiten (Massenaktion)

POST /api/v1/inbox/messages/actions
Scope write Tarif: inbox Idempotency-Key

Dieselben Aktionen wie in der App, für bis zu 500 Mails auf einmal – Verschieben, Löschen und Markierungen werden auf den Mailserver übertragen. params: folder_id (move), until (snooze), at (follow_up = „Erinnern, falls keine Antwort kommt“), labels + mode set|add|remove (label), category (set_category). delete_forever löscht endgültig – auch auf dem Server.

Request-Body application/json

FeldBeschreibung
ids array<integer> Pflicht IDs der Mails (max. 500).
action string Pflicht Aktion. Werte: read, unread, flag, unflag, archive, trash, spam, notspam, move, restore, delete_forever, snooze, unsnooze, follow_up, clear_follow_up, label, set_category
params object optional Parameter der Aktion (siehe Beschreibung).

Antwort

200 OK

Antwortfelder
FeldBeschreibung
action string Ausgeführte Aktion.
affected integer Geänderte Mails.
ids array<integer> IDs der bearbeiteten Mails.
not_found array IDs, die es (für Dich) nicht gibt.
warning string · null Hinweis, wenn der Mailserver die Änderung nicht übernommen hat (der nächste Abruf gleicht ab), sonst null.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 422 validation_error 429 rate_limited

POST /inbox/messages/actions
curl -X POST https://persolz.com/api/v1/inbox/messages/actions \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "ids": [9120, 9121],
    "action": "snooze",
    "params": {
      "until": "2026-10-12T09:00:00+02:00"
    }
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/messages/actions');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'ids' => [9120, 9121],
        'action' => 'snooze',
        'params' => [
            'until' => '2026-10-12T09:00:00+02:00',
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/messages/actions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    ids: [9120, 9121],
    action: 'snooze',
    params: {
      until: '2026-10-12T09:00:00+02:00'
    }
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/inbox/messages/actions",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "ids": [9120, 9121],
        "action": "snooze",
        "params": {
            "until": "2026-10-12T09:00:00+02:00",
        },
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "action": "snooze",
    "affected": 2,
    "ids": [9120, 9121],
    "not_found": [],
    "warning": null
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Auf eine Mail antworten

POST /api/v1/inbox/messages/{id}/reply
Scope send Tarif: inbox Idempotency-Key

Antwortet über das Postfach der Mail (SMTP-Konto des Postfachs) – mit In-Reply-To/References, Signatur und zitiertem Original (quote=false lässt das Zitat weg). Empfänger und Betreff („Re: …“) werden wie in der App vorbelegt und können überschrieben werden; reply_all=true antwortet allen. Ohne send_at wird sofort gesendet; die Kopie landet im Ordner „Gesendet“. Schlägt der Versand fehl, antwortet die API mit 503 – der Entwurf bleibt (draft_id) und kann mit POST /inbox/drafts/{id}/send erneut gesendet werden.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
html string optional Inhalt als HTML (wird bereinigt; Skripte, Formulare & Co. werden entfernt). Signatur des Postfachs wird automatisch ergänzt.
text string optional Alternativ: reiner Text (Links werden verlinkt).
attachment_ids array<integer> optional Anhänge aus der Anhang-Cloud (GET /attachments).
send_at string optional Später senden (ISO 8601, mind. 1 Minute, max. 1 Jahr in der Zukunft). Leer = sofort.
to string|array optional Empfänger: E-Mail, kommagetrennte E-Mails („Name <mail@…>“ erlaubt) oder Liste aus E-Mails bzw. Objekten {email, name}.
cc string|array optional Kopie (wie to).
bcc string|array optional Blindkopie (wie to).
subject string optional Betreff (Standard: „Re: <Betreff>“).
reply_all boolean optional Allen antworten (To + Cc der Mail, ohne Deine eigenen Adressen).
quote boolean optional Original zitieren (Standard true).

Antwort

201 Gesendet bzw. geplant – ein Entwurfs-Objekt (Posteingang)

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited 503 service_unavailable

POST /inbox/messages/{id}/reply
curl -X POST https://persolz.com/api/v1/inbox/messages/42/reply \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p><p>Viele Grüße</p>"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/messages/42/reply');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'html' => '<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p><p>Viele Grüße</p>',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/messages/42/reply', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    html: '<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p><p>Viele Grüße</p>'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/messages/42/reply",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "html": "<p>Hallo Anna,</p><p>herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € und sind in 10 Tagen bei Dir.</p><p>Viele Grüße</p>",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 5502,
    "mailbox_id": 2,
    "kind": "reply",
    "kind_label": "Antwort",
    "status": "sent",
    "status_label": "Gesendet",
    "source": "user",
    "reply_to_message_id": 9120,
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "to": [
      {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      }
    ],
    "cc": [],
    "bcc": [],
    "subject": "Re: Frage zu Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
    "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
    "attachment_ids": [],
    "mail_attachment_ids": [],
    "send_at": "2026-10-09T14:21:00+02:00",
    "sent_at": "2026-10-09T14:21:04+02:00",
    "sent_message_id": "<6f1c0d9a2b7e4c1f@kartenherz.com>",
    "error": null,
    "ai": null,
    "created_at": "2026-10-09T14:06:10+02:00",
    "updated_at": "2026-10-09T14:06:10+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

KI-Antwort entwerfen

POST /api/v1/inbox/messages/{id}/ai-draft
Scope ai Tarif: inbox Tarif: ai Idempotency-Key

Die KI schreibt eine Antwort im Stil des Postfachs (Ton, Wissen für die KI, Kontaktdaten, Anrede Du/Sie und Sprache des Absenders) – inklusive Selbsteinschätzung (confidence), Risiken und Begründung. Die Mail wird als nicht vertrauenswürdige Eingabe behandelt (Schutz vor Prompt-Injection). Ein unveränderter KI-Entwurf zur selben Mail wird ohne neue KI-Anfrage wiederverwendet (cached), außer bei fresh=true oder eigenen Vorgaben. save=draft speichert ihn als Vorschlag („Antwort bereit“), save=approval legt ihn in die Freigaben – senden mit POST /inbox/approvals/{id}/approve. Verbraucht KI-Guthaben.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
instruction string optional Eigene Vorgabe (max. 600 Zeichen), z. B. „Biete einen Rückruf an“.
tone string optional Ton: auto, freundlich, herzlich, locker, foermlich, sachlich, knapp.
length string optional Länge: auto, kurz, mittel, lang.
include_slots boolean optional Freie Termine aus dem Terminbuch vorschlagen.
model string optional sonnet (Standard des Postfachs) oder haiku. Werte: sonnet, haiku
fresh boolean optional Immer neu schreiben (keinen vorhandenen Entwurf wiederverwenden).
save string optional none (Standard), draft (Vorschlag speichern) oder approval (in die Freigaben). Werte: none, draft, approval

Antwort

200 OK

Alle 15 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
subject string Betreff.
html string HTML-Inhalt.
text string Text.
confidence integer Selbsteinschätzung der KI (0–100).
risk_flags array Risiken: legal, money, medical, threat, prompt_injection, personal_data, unclear.
risk_flag_labels array Risiken in Worten.
needs_human boolean Die KI empfiehlt, die Antwort selbst zu prüfen.
reason string Begründung der KI.
language string Sprache der Antwort.
form_of_address string Anrede der Antwort: du oder sie.
slots array Vorgeschlagene freie Termine (mit include_slots).
model string Verwendetes KI-Modell.
cost_eur number Kosten dieser KI-Anfrage in Euro (wird vom KI-Guthaben abgezogen).
cached boolean Ein vorhandener KI-Entwurf wurde ohne neue KI-Anfrage wiederverwendet.
draft_id integer Gespeicherter Entwurf (save=draft|approval oder wiederverwendet) oder null.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited 503 service_unavailable

POST /inbox/messages/{id}/ai-draft
curl -X POST https://persolz.com/api/v1/inbox/messages/42/ai-draft \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "tone": "herzlich",
    "instruction": "Biete Muster per Post an.",
    "save": "approval"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/messages/42/ai-draft');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'tone' => 'herzlich',
        'instruction' => 'Biete Muster per Post an.',
        'save' => 'approval',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/messages/42/ai-draft', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    tone: 'herzlich',
    instruction: 'Biete Muster per Post an.',
    save: 'approval'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/messages/42/ai-draft",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "tone": "herzlich",
        "instruction": "Biete Muster per Post an.",
        "save": "approval",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "subject": "Re: Frage zu Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>herzlichen Glückwunsch! …</p>",
    "text": "Hallo Anna,\n\nherzlichen Glückwunsch! …",
    "confidence": 88,
    "risk_flags": [],
    "risk_flag_labels": [],
    "needs_human": false,
    "reason": "Preis und Lieferzeit stehen im Wissen für die KI.",
    "language": "de",
    "form_of_address": "du",
    "slots": [],
    "model": "claude-sonnet-5-5",
    "cost_eur": 0.0041,
    "cached": false,
    "draft_id": 5502
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Mail schreiben (Entwurf, senden oder planen)

POST /api/v1/inbox/drafts
Scope send Tarif: inbox Idempotency-Key

Legt eine ausgehende Mail an und sendet sie sofort (Standard), zum Zeitpunkt send_at oder gar nicht (send=false → Entwurf). kind: new (neue Mail), reply / reply_all / forward (mit message_id – Empfänger, Betreff, Zitat und bei forward die Originalanhänge werden vorbelegt). Absender ist das Postfach (mailbox_id; bei message_id dessen Postfach).

Request-Body application/json

FeldBeschreibung
mailbox_id integer optional Absender-Postfach (Pflicht ohne message_id).
kind string optional new (Standard), reply, reply_all oder forward. Werte: new, reply, reply_all, forward
message_id integer optional Bezugsmail für reply, reply_all und forward.
subject string optional Betreff (bei new Pflicht zum Senden).
to string|array optional Empfänger: E-Mail, kommagetrennte E-Mails („Name <mail@…>“ erlaubt) oder Liste aus E-Mails bzw. Objekten {email, name}.
cc string|array optional Kopie (wie to).
bcc string|array optional Blindkopie (wie to).
html string optional Inhalt als HTML (wird bereinigt; Skripte, Formulare & Co. werden entfernt). Signatur des Postfachs wird automatisch ergänzt.
text string optional Alternativ: reiner Text (Links werden verlinkt).
attachment_ids array<integer> optional Anhänge aus der Anhang-Cloud (GET /attachments).
send_at string optional Später senden (ISO 8601, mind. 1 Minute, max. 1 Jahr in der Zukunft). Leer = sofort.
mail_attachment_ids array<integer> optional forward: Anhänge der Originalmail (Standard: alle).
quote boolean optional reply/forward: Original zitieren (Standard true).
send boolean optional false = nur als Entwurf speichern (Standard true).

Antwort

201 Angelegt (gesendet, geplant oder Entwurf) – ein Entwurfs-Objekt (Posteingang)

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited 503 service_unavailable

POST /inbox/drafts
curl -X POST https://persolz.com/api/v1/inbox/drafts \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "mailbox_id": 2,
    "to": ["anna@beispiel.de"],
    "subject": "Dein Angebot für Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>anbei Dein Angebot …</p>",
    "attachment_ids": [9],
    "send_at": "2026-10-10T09:00:00+02:00"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/drafts');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'mailbox_id' => 2,
        'to' => ['anna@beispiel.de'],
        'subject' => 'Dein Angebot für Hochzeitskarten',
        'html' => '<p>Hallo Anna,</p><p>anbei Dein Angebot …</p>',
        'attachment_ids' => [9],
        'send_at' => '2026-10-10T09:00:00+02:00',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/drafts', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    mailbox_id: 2,
    to: ['anna@beispiel.de'],
    subject: 'Dein Angebot für Hochzeitskarten',
    html: '<p>Hallo Anna,</p><p>anbei Dein Angebot …</p>',
    attachment_ids: [9],
    send_at: '2026-10-10T09:00:00+02:00'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/drafts",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "mailbox_id": 2,
        "to": ["anna@beispiel.de"],
        "subject": "Dein Angebot für Hochzeitskarten",
        "html": "<p>Hallo Anna,</p><p>anbei Dein Angebot …</p>",
        "attachment_ids": [9],
        "send_at": "2026-10-10T09:00:00+02:00",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 5502,
    "mailbox_id": 2,
    "kind": "new",
    "kind_label": "Neue Mail",
    "status": "scheduled",
    "status_label": "Geplant",
    "source": "user",
    "reply_to_message_id": null,
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "to": [
      {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      }
    ],
    "cc": [],
    "bcc": [],
    "subject": "Dein Angebot für Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
    "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
    "attachment_ids": [9],
    "mail_attachment_ids": [],
    "send_at": "2026-10-10T09:00:00+02:00",
    "sent_at": null,
    "sent_message_id": null,
    "error": null,
    "ai": null,
    "created_at": "2026-10-09T14:06:10+02:00",
    "updated_at": "2026-10-09T14:06:10+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Entwurf / ausgehende Mail abrufen

GET /api/v1/inbox/drafts/{id}
Scope read Tarif: inbox

Status einer ausgehenden Mail: draft, pending (KI-Entwurf wartet auf Freigabe), scheduled (geplant oder im Rückgängig-Fenster), sending, sent, failed (error), rejected, cancelled.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Entwurfs-Objekt (Posteingang)

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /inbox/drafts/{id}
curl https://persolz.com/api/v1/inbox/drafts/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/drafts/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/drafts/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/inbox/drafts/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 5502,
    "mailbox_id": 2,
    "kind": "reply",
    "kind_label": "Antwort",
    "status": "sent",
    "status_label": "Gesendet",
    "source": "user",
    "reply_to_message_id": 9120,
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "to": [
      {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      }
    ],
    "cc": [],
    "bcc": [],
    "subject": "Re: Frage zu Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
    "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
    "attachment_ids": [],
    "mail_attachment_ids": [],
    "send_at": "2026-10-09T14:21:00+02:00",
    "sent_at": "2026-10-09T14:21:04+02:00",
    "sent_message_id": "<6f1c0d9a2b7e4c1f@kartenherz.com>",
    "error": null,
    "ai": null,
    "created_at": "2026-10-09T14:06:10+02:00",
    "updated_at": "2026-10-09T14:06:10+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Entwurf senden

POST /api/v1/inbox/drafts/{id}/send
Scope send Tarif: inbox Idempotency-Key

Sendet einen Entwurf (auch einen fehlgeschlagenen oder gestoppten) sofort oder zum Zeitpunkt send_at. KI-Entwürfe gibst Du mit POST /inbox/approvals/{id}/approve frei.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
send_at string optional Später senden (ISO 8601). Leer = sofort.

Antwort

200 OK – ein Entwurfs-Objekt (Posteingang)

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited 503 service_unavailable

POST /inbox/drafts/{id}/send
curl -X POST https://persolz.com/api/v1/inbox/drafts/42/send \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{}'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/drafts/42/send');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode((object) []),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/drafts/42/send', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({})
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/drafts/42/send",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={},
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 5502,
    "mailbox_id": 2,
    "kind": "reply",
    "kind_label": "Antwort",
    "status": "sent",
    "status_label": "Gesendet",
    "source": "user",
    "reply_to_message_id": 9120,
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "to": [
      {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      }
    ],
    "cc": [],
    "bcc": [],
    "subject": "Re: Frage zu Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
    "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
    "attachment_ids": [],
    "mail_attachment_ids": [],
    "send_at": "2026-10-09T14:21:00+02:00",
    "sent_at": "2026-10-09T14:21:04+02:00",
    "sent_message_id": "<6f1c0d9a2b7e4c1f@kartenherz.com>",
    "error": null,
    "ai": null,
    "created_at": "2026-10-09T14:06:10+02:00",
    "updated_at": "2026-10-09T14:06:10+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Geplanten Versand stoppen

POST /api/v1/inbox/drafts/{id}/cancel
Scope send Tarif: inbox Idempotency-Key

Stoppt eine geplante Mail (auch eine Autopilot-Antwort in der Wartezeit). Eigene Mails werden wieder Entwürfe, KI-Antworten landen wieder in den Freigaben. 409, wenn die Mail schon unterwegs ist.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Entwurfs-Objekt (Posteingang)

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 429 rate_limited

POST /inbox/drafts/{id}/cancel
curl -X POST https://persolz.com/api/v1/inbox/drafts/42/cancel \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/drafts/42/cancel');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/drafts/42/cancel', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Idempotency-Key': crypto.randomUUID()
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/drafts/42/cancel",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 5502,
    "mailbox_id": 2,
    "kind": "reply",
    "kind_label": "Antwort",
    "status": "draft",
    "status_label": "Entwurf",
    "source": "user",
    "reply_to_message_id": 9120,
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "to": [
      {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      }
    ],
    "cc": [],
    "bcc": [],
    "subject": "Re: Frage zu Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
    "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
    "attachment_ids": [],
    "mail_attachment_ids": [],
    "send_at": null,
    "sent_at": null,
    "sent_message_id": null,
    "error": null,
    "ai": null,
    "created_at": "2026-10-09T14:06:10+02:00",
    "updated_at": "2026-10-09T14:06:10+02:00"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Freigaben auflisten

GET /api/v1/inbox/approvals
Scope read Tarif: inbox paginiert

KI-Entwürfe mit Auszug der Originalmail und den Gründen, warum der Autopilot sie nicht selbst gesendet hat (reasons). status: pending (Standard – wartet auf Freigabe), scheduled (geplant, z. B. Autopilot in der Wartezeit), sent (von der KI geschrieben und gesendet), rejected, draft (Vorschläge „Antwort bereit“), all.

Query-Parameter

NameBeschreibung
status string optional pending | scheduled | sent | rejected | draft | all Werte: pending, scheduled, sent, rejected, draft, all Beispiel: pending
mailbox_id integer optional Nur dieses Postfach. Beispiel: 2
q string optional Suche in Betreff und Absender. Beispiel: hochzeit
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Entwurfs-Objekt (Posteingang)en, zusätzlich:

Alle 13 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
reasons array Gründe, warum der Autopilot nicht selbst gesendet hat.
message object Die Originalmail (Auszug).
message.id integer Eindeutige ID.
message.subject string Betreff.
message.from object Absender.
message.from.email string E-Mail-Adresse des Absenders.
message.from.name string Name des Absenders (kann leer sein).
message.received_at string (Datum/Zeit) Eingang (ISO 8601).
message.summary string Zusammenfassung in einem Satz (KI) oder null.
message.category string KI-Kategorie: lead, customer, appointment, invoice, order, campaign_reply, partner, personal, newsletter, notification, unsubscribe, complaint, spam, other – oder null (noch nicht eingeordnet).
message.category_label string Kategorie in Worten.
message.priority string Priorität laut KI: high, normal oder low – oder null.
message.excerpt string Auszug aus dem Text der Originalmail.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /inbox/approvals
curl "https://persolz.com/api/v1/inbox/approvals?status=pending" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'pending',
]);
$ch = curl_init('https://persolz.com/api/v1/inbox/approvals?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'pending'
});
const response = await fetch('https://persolz.com/api/v1/inbox/approvals?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/inbox/approvals",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "pending",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 5502,
      "mailbox_id": 2,
      "kind": "reply",
      "kind_label": "Antwort",
      "status": "pending",
      "status_label": "Wartet auf Freigabe",
      "source": "ai",
      "reply_to_message_id": 9120,
      "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
      "to": [
        {
          "email": "anna@beispiel.de",
          "name": "Anna Schmidt"
        }
      ],
      "cc": [],
      "bcc": [],
      "subject": "Re: Frage zu Hochzeitskarten",
      "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
      "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
      "attachment_ids": [],
      "mail_attachment_ids": [],
      "send_at": null,
      "sent_at": null,
      "sent_message_id": null,
      "error": null,
      "ai": {
        "model": "claude-sonnet-5-5",
        "confidence": 88,
        "reason": "Preis und Lieferzeit stehen im Wissen für die KI.",
        "flags": [],
        "flag_labels": [],
        "rating": null,
        "edited": false
      },
      "created_at": "2026-10-09T14:06:10+02:00",
      "updated_at": "2026-10-09T14:06:10+02:00",
      "reasons": [],
      "message": {
        "id": 9120,
        "subject": "Frage zu Hochzeitskarten",
        "from": {
          "email": "anna@beispiel.de",
          "name": "Anna Schmidt"
        },
        "received_at": "2026-10-09T14:05:00+02:00",
        "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
        "category": "lead",
        "category_label": "Leads & Anfragen",
        "priority": "high",
        "excerpt": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung. Was kostet das und wie lange dauert die Lieferung?"
      }
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

KI-Entwurf freigeben und senden

POST /api/v1/inbox/approvals/{id}/approve
Scope send Tarif: inbox Idempotency-Key

Gibt einen KI-Entwurf frei und sendet ihn sofort – optional mit Änderungen (subject, html/text, to, cc, bcc, attachment_ids). Deine Änderungen fließen als Erkenntnisse in künftige KI-Entwürfe ein.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
subject string optional Geänderter Betreff.
html string optional Geänderter Text als HTML.
text string optional Alternativ: geänderter reiner Text.
to string|array optional Geänderte Empfänger.
cc string|array optional Kopie.
bcc string|array optional Blindkopie.
attachment_ids array<integer> optional Anhänge aus der Anhang-Cloud.

Antwort

200 OK – ein Entwurfs-Objekt (Posteingang), zusätzlich:

Alle 13 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
reasons array Gründe, warum der Autopilot nicht selbst gesendet hat.
message object Die Originalmail (Auszug).
message.id integer Eindeutige ID.
message.subject string Betreff.
message.from object Absender.
message.from.email string E-Mail-Adresse des Absenders.
message.from.name string Name des Absenders (kann leer sein).
message.received_at string (Datum/Zeit) Eingang (ISO 8601).
message.summary string Zusammenfassung in einem Satz (KI) oder null.
message.category string KI-Kategorie: lead, customer, appointment, invoice, order, campaign_reply, partner, personal, newsletter, notification, unsubscribe, complaint, spam, other – oder null (noch nicht eingeordnet).
message.category_label string Kategorie in Worten.
message.priority string Priorität laut KI: high, normal oder low – oder null.
message.excerpt string Auszug aus dem Text der Originalmail.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited 503 service_unavailable

POST /inbox/approvals/{id}/approve
curl -X POST https://persolz.com/api/v1/inbox/approvals/42/approve \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<p>Hallo Anna,</p><p>herzlichen Glückwunsch! Ich schicke Dir gern Muster per Post …</p>"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/approvals/42/approve');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'html' => '<p>Hallo Anna,</p><p>herzlichen Glückwunsch! Ich schicke Dir gern Muster per Post …</p>',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/approvals/42/approve', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    html: '<p>Hallo Anna,</p><p>herzlichen Glückwunsch! Ich schicke Dir gern Muster per Post …</p>'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/inbox/approvals/42/approve",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "html": "<p>Hallo Anna,</p><p>herzlichen Glückwunsch! Ich schicke Dir gern Muster per Post …</p>",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 5502,
    "mailbox_id": 2,
    "kind": "reply",
    "kind_label": "Antwort",
    "status": "sent",
    "status_label": "Gesendet",
    "source": "ai",
    "reply_to_message_id": 9120,
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "to": [
      {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      }
    ],
    "cc": [],
    "bcc": [],
    "subject": "Re: Frage zu Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
    "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
    "attachment_ids": [],
    "mail_attachment_ids": [],
    "send_at": "2026-10-09T14:21:00+02:00",
    "sent_at": "2026-10-09T14:21:04+02:00",
    "sent_message_id": null,
    "error": null,
    "ai": {
      "model": "claude-sonnet-5-5",
      "confidence": 88,
      "reason": "Preis und Lieferzeit stehen im Wissen für die KI.",
      "flags": [],
      "flag_labels": [],
      "rating": null,
      "edited": true
    },
    "created_at": "2026-10-09T14:06:10+02:00",
    "updated_at": "2026-10-09T14:06:10+02:00",
    "reasons": [],
    "message": {
      "id": 9120,
      "subject": "Frage zu Hochzeitskarten",
      "from": {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      },
      "received_at": "2026-10-09T14:05:00+02:00",
      "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
      "category": "lead",
      "category_label": "Leads & Anfragen",
      "priority": "high",
      "excerpt": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung. Was kostet das und wie lange dauert die Lieferung?"
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

KI-Entwurf ablehnen

POST /api/v1/inbox/approvals/{id}/reject
Scope write Tarif: inbox Idempotency-Key

Lehnt einen KI-Entwurf ab. Typische Gründe („zu lang“, „falscher Ton“, „stimmt nicht“) werden zu Erkenntnissen für künftige Entwürfe.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
reason string optional Grund (optional, max. 250 Zeichen).

Antwort

200 OK – ein Entwurfs-Objekt (Posteingang), zusätzlich:

Alle 13 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
reasons array Gründe, warum der Autopilot nicht selbst gesendet hat.
message object Die Originalmail (Auszug).
message.id integer Eindeutige ID.
message.subject string Betreff.
message.from object Absender.
message.from.email string E-Mail-Adresse des Absenders.
message.from.name string Name des Absenders (kann leer sein).
message.received_at string (Datum/Zeit) Eingang (ISO 8601).
message.summary string Zusammenfassung in einem Satz (KI) oder null.
message.category string KI-Kategorie: lead, customer, appointment, invoice, order, campaign_reply, partner, personal, newsletter, notification, unsubscribe, complaint, spam, other – oder null (noch nicht eingeordnet).
message.category_label string Kategorie in Worten.
message.priority string Priorität laut KI: high, normal oder low – oder null.
message.excerpt string Auszug aus dem Text der Originalmail.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 409 conflict 422 validation_error 429 rate_limited

POST /inbox/approvals/{id}/reject
curl -X POST https://persolz.com/api/v1/inbox/approvals/42/reject \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Zu lang"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/approvals/42/reject');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'reason' => 'Zu lang',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/approvals/42/reject', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    reason: 'Zu lang'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/inbox/approvals/42/reject",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "reason": "Zu lang",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 5502,
    "mailbox_id": 2,
    "kind": "reply",
    "kind_label": "Antwort",
    "status": "rejected",
    "status_label": "Abgelehnt",
    "source": "ai",
    "reply_to_message_id": 9120,
    "thread_key": "3f1c9a7e5b2d4c8f9e0a1d2b3c4d5e6f70819a2b",
    "to": [
      {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      }
    ],
    "cc": [],
    "bcc": [],
    "subject": "Re: Frage zu Hochzeitskarten",
    "html": "<p>Hallo Anna,</p><p>wie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …</p>",
    "text": "Hallo Anna,\n\nwie schön, herzlichen Glückwunsch! 80 Karten mit Goldprägung kosten 236 € …",
    "attachment_ids": [],
    "mail_attachment_ids": [],
    "send_at": null,
    "sent_at": null,
    "sent_message_id": null,
    "error": null,
    "ai": {
      "model": "claude-sonnet-5-5",
      "confidence": 88,
      "reason": "Preis und Lieferzeit stehen im Wissen für die KI.",
      "flags": [],
      "flag_labels": [],
      "rating": null,
      "edited": false
    },
    "created_at": "2026-10-09T14:06:10+02:00",
    "updated_at": "2026-10-09T14:06:10+02:00",
    "reasons": [],
    "message": {
      "id": 9120,
      "subject": "Frage zu Hochzeitskarten",
      "from": {
        "email": "anna@beispiel.de",
        "name": "Anna Schmidt"
      },
      "received_at": "2026-10-09T14:05:00+02:00",
      "summary": "Anna möchte 80 Einladungskarten für ihre Hochzeit im Mai und fragt nach Preis und Lieferzeit.",
      "category": "lead",
      "category_label": "Leads & Anfragen",
      "priority": "high",
      "excerpt": "Hallo, wir heiraten im Mai und suchen 80 Einladungskarten mit Goldprägung. Was kostet das und wie lange dauert die Lieferung?"
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Posteingang-Kennzahlen

GET /api/v1/inbox/stats
Scope read Tarif: inbox

Zähler wie in der App (Ansichten, Kategorien, Freigaben; badge = Zahl am Menüpunkt: ungelesen im Fokus + Freigaben), Status der Postfächer, eingegangene, gesendete und beantwortete Mails im Zeitraum sowie die KI-Statistik: Entwürfe, Freigaben, Bearbeitungs- und Freigabequote, Autopilot-Antworten, geschätzte gesparte Zeit und Entwürfe pro Tag.

Query-Parameter

NameBeschreibung
days integer optional Zeitraum in Tagen (1–365, Standard 30). Beispiel: 30

Antwort

200 OK

Alle 62 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
period object Zeitraum der Kennzahlen.
period.days integer Tage.
period.from string (Datum) Erster Tag.
period.to string (Datum) Letzter Tag (heute).
mailboxes object Postfächer nach Status.
mailboxes.total integer Postfächer gesamt.
mailboxes.ok integer Ohne Fehler.
mailboxes.error integer Mit Fehler beim Abruf.
mailboxes.paused integer Pausiert.
mailboxes.last_sync_at string (Datum/Zeit) Letzter Abruf eines Postfachs oder null.
views object Zähler der Ansichten wie in der App (focus_unread = ungelesen im Fokus).
views.focus integer Anzahl Mails in dieser Ansicht.
views.focus_unread integer Anzahl Mails in dieser Ansicht.
views.all integer Anzahl Mails in dieser Ansicht.
views.unread integer Anzahl Mails in dieser Ansicht.
views.flagged integer Anzahl Mails in dieser Ansicht.
views.waiting integer Anzahl Mails in dieser Ansicht.
views.snoozed integer Anzahl Mails in dieser Ansicht.
views.approvals integer Anzahl Mails in dieser Ansicht.
views.scheduled integer Anzahl Mails in dieser Ansicht.
views.failed integer Anzahl Mails in dieser Ansicht.
views.drafts integer Anzahl Mails in dieser Ansicht.
views.sent integer Anzahl Mails in dieser Ansicht.
views.archive integer Anzahl Mails in dieser Ansicht.
views.trash integer Anzahl Mails in dieser Ansicht.
views.spam integer Anzahl Mails in dieser Ansicht.
categories object Mails im Posteingang je KI-Kategorie.
categories.lead object Kategorie.
categories.lead.label string Kategorie in Worten.
categories.lead.total integer Mails gesamt.
categories.lead.unread integer Davon ungelesen.
categories.campaign_reply object Kategorie.
categories.campaign_reply.label string Kategorie in Worten.
categories.campaign_reply.total integer Mails gesamt.
categories.campaign_reply.unread integer Davon ungelesen.
approvals integer KI-Entwürfe, die auf Deine Freigabe warten.
badge integer Zahl am Menüpunkt „Posteingang“ (ungelesen im Fokus + Freigaben).
mails object Mails im Zeitraum.
mails.received integer Eingegangen.
mails.sent integer Aus Persolz gesendet.
mails.replied integer Beantwortet.
ai object KI im Posteingang.
ai.drafts integer KI-Entwürfe im Zeitraum.
ai.approved integer Von Dir freigegeben.
ai.edited integer Davon vor dem Senden geändert.
ai.rejected integer Abgelehnt.
ai.auto_sent integer Vom Autopiloten gesendet.
ai.auto_planned integer Vom Autopiloten geplant (Wartezeit läuft).
ai.pending integer Warten aktuell auf Freigabe.
ai.thumbs_up integer Mit „gut“ bewertet.
ai.thumbs_down integer Mit „schlecht“ bewertet.
ai.approval_rate number Freigabequote (freigegeben / entschieden) oder null.
ai.edit_rate number Anteil der vor dem Senden geänderten Freigaben oder null.
ai.avg_confidence integer Durchschnittliche Selbsteinschätzung der KI oder null.
ai.triaged integer Von der KI eingeordnete Mails.
ai.minutes_saved integer Geschätzt gesparte Minuten.
ai.hours_saved number Geschätzt gesparte Stunden.
ai.hours_saved_label string Gesparte Zeit in Worten.
ai.per_day array<object> KI-Entwürfe pro Tag.
ai.per_day[].date string (Datum) Tag.
ai.per_day[].drafts integer KI-Entwürfe.
ai.per_day[].auto integer Davon Autopilot.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /inbox/stats
curl https://persolz.com/api/v1/inbox/stats \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/inbox/stats');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/inbox/stats', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/inbox/stats",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "period": {
      "days": 30,
      "from": "2026-09-10",
      "to": "2026-10-09"
    },
    "mailboxes": {
      "total": 2,
      "ok": 2,
      "error": 0,
      "paused": 0,
      "last_sync_at": "2026-10-09T14:05:00+02:00"
    },
    "views": {
      "focus": 7,
      "focus_unread": 3,
      "all": 214,
      "unread": 5,
      "flagged": 4,
      "waiting": 2,
      "snoozed": 1,
      "approvals": 2,
      "scheduled": 1,
      "failed": 0,
      "drafts": 1,
      "sent": 96,
      "archive": 812,
      "trash": 12,
      "spam": 0
    },
    "categories": {
      "lead": {
        "label": "Leads & Anfragen",
        "total": 18,
        "unread": 2
      },
      "campaign_reply": {
        "label": "Leser-Antworten",
        "total": 9,
        "unread": 1
      }
    },
    "approvals": 2,
    "badge": 5,
    "mails": {
      "received": 320,
      "sent": 104,
      "replied": 87
    },
    "ai": {
      "drafts": 41,
      "approved": 33,
      "edited": 9,
      "rejected": 3,
      "auto_sent": 12,
      "auto_planned": 1,
      "pending": 2,
      "thumbs_up": 6,
      "thumbs_down": 1,
      "approval_rate": 0.9167,
      "edit_rate": 0.2727,
      "avg_confidence": 84,
      "triaged": 298,
      "minutes_saved": 462,
      "hours_saved": 7.7,
      "hours_saved_label": "ca. 7,7 Stunden",
      "per_day": [
        {
          "date": "2026-10-08",
          "drafts": 3,
          "auto": 1
        },
        {
          "date": "2026-10-09",
          "drafts": 2,
          "auto": 0
        }
      ]
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

KI

KI-Texte erzeugen und den KI-Verbrauch abfragen.

KI-Anfragen verbrauchen Dein monatliches KI-Guthaben. Den Stand zeigt GET /ai/usage.

KI-Text erzeugen

POST /api/v1/ai/generate
Scope ai Tarif: ai Idempotency-Key

Schreibt oder überarbeitet Texte mit der Persolz-KI (wie im Editor): improve, shorten, expand, subject_ideas (8 Betreffzeilen), preheader (3 Vorschläge), urgency, story, emotional, fix, translate_en, custom. Statt action kannst Du einfach „prompt“ senden (= custom). Verbraucht KI-Guthaben; 30 Anfragen pro Minute.

Request-Body application/json

FeldBeschreibung
action string optional Aktion (siehe Beschreibung). Werte: improve, shorten, expand, subject_ideas, preheader, urgency, story, emotional, fix, translate_en, custom
prompt string optional Freie Anweisung (setzt action=custom).
text string optional Ausgangstext.
html string optional Alternativ: HTML-Fragment (Struktur bleibt erhalten).
instruction string optional Zusätzlicher Wunsch.
subject string optional Betreff als Kontext.
context string optional Hintergrund (max. 4000 Zeichen).
scope string optional seinfeld | soap | newsletter | email (nutzt passende A/B-Erkenntnisse).
model string optional haiku (Standard, schnell) | sonnet (beste Qualität)

Antwort

200 OK

Alle 10 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
action string Ausgeführte Aktion.
text string Erzeugter Text.
html string · null Erzeugtes HTML (wenn HTML gesendet wurde) oder null.
data object Strukturierte Zusatzdaten.
data.ideas array<string> Vorschläge (z. B. 8 Betreffzeilen bei subject_ideas).
cost_eur number Kosten dieser KI-Anfrage in Euro (wird vom KI-Guthaben abgezogen).
remaining_eur number Verbleibendes KI-Guthaben in Euro.
model string Verwendetes KI-Modell.
provider string KI-Anbieter: anthropic oder openai.
fallback_used boolean Der Ersatz-Anbieter hat geantwortet, weil der Haupt-Anbieter nicht erreichbar war.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited 503 service_unavailable

POST /ai/generate
curl -X POST https://persolz.com/api/v1/ai/generate \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Idempotency-Key: $(openssl rand -hex 16)" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "subject_ideas",
    "text": "Unser Herbst-Workshop findet am 20.10. statt – nur 12 Plätze.",
    "scope": "newsletter"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/ai/generate');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
        'Idempotency-Key: ' . bin2hex(random_bytes(16)),
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'action' => 'subject_ideas',
        'text' => 'Unser Herbst-Workshop findet am 20.10. statt – nur 12 Plätze.',
        'scope' => 'newsletter',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/ai/generate', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID()
  },
  body: JSON.stringify({
    action: 'subject_ideas',
    text: 'Unser Herbst-Workshop findet am 20.10. statt – nur 12 Plätze.',
    scope: 'newsletter'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests
import uuid

response = requests.post(
    "https://persolz.com/api/v1/ai/generate",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "action": "subject_ideas",
        "text": "Unser Herbst-Workshop findet am 20.10. statt – nur 12 Plätze.",
        "scope": "newsletter",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "action": "subject_ideas",
    "text": "Nur 12 Plätze: Dein Herbst-Workshop\n…",
    "html": null,
    "data": {
      "ideas": ["Nur 12 Plätze: Dein Herbst-Workshop", "…"]
    },
    "cost_eur": 0.0012,
    "remaining_eur": 7.68,
    "model": "claude-haiku-5-5",
    "provider": "anthropic",
    "fallback_used": false
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

KI-Verbrauch

GET /api/v1/ai/usage
Scope read

Guthaben, Verbrauch und Anfragen im laufenden Monat.

Antwort

200 OK

Alle 10 Antwortfelder anzeigen
Antwortfelder
FeldBeschreibung
credit number Monatliches KI-Guthaben in Euro.
used number Im laufenden Monat verbraucht (Euro).
remaining number Verbleibendes KI-Guthaben in Euro.
requests integer KI-Anfragen im laufenden Monat.
limit integer Max. KI-Anfragen pro Monat (0 = unbegrenzt).
period string Abrechnungsmonat (YYYY-MM).
bonus number Zusätzliches Bonus-Guthaben in Euro.
percent number Verbrauch in Prozent des Guthabens.
allowed boolean KI ist gerade nutzbar (Tarif und Guthaben).
configured boolean Die KI ist auf dem Server eingerichtet.

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /ai/usage
curl https://persolz.com/api/v1/ai/usage \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/ai/usage');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/ai/usage', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/ai/usage",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "credit": 10.0,
    "used": 2.31,
    "remaining": 7.69,
    "requests": 12,
    "limit": 0,
    "period": "2026-10",
    "bonus": 0.0,
    "percent": 23.1,
    "allowed": true,
    "configured": true
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Webhooks

Webhooks verwalten, testen und Zustellungen einsehen.

Wie Du Webhooks empfängst und die Signatur prüfst, steht im Abschnitt Webhooks.

Objekte: Webhook-Objekt · Zustellungs-Objekt

Das Webhook-Objekt

Ein Webhook ohne Secret – das Secret gibt es nur beim Anlegen und beim Erneuern.

Attribute

Das Webhook-Objekt
FeldBeschreibung
id integer Eindeutige ID.
url string Deine Empfänger-URL (https).
description string Beschreibung.
events array<string> Abonnierte Ereignisse (["*"] = alle).
is_active boolean Webhook ist aktiv.
failure_count integer Fehlschläge in Folge – nach 20 wird der Webhook pausiert.
last_status integer Letzter HTTP-Status Deines Endpunkts oder null.
last_error string · null Letzte Fehlermeldung oder null.
last_called_at string (Datum/Zeit) Letzter Aufruf oder null.
disabled_at string · null Zeitpunkt der automatischen Pausierung oder null.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
updated_at string · null Zeitpunkt der letzten Änderung (ISO 8601) oder null.
secret_hint string Anfang des Secrets zum Wiedererkennen.
Das Webhook-Objekt JSON
{
  "id": 4,
  "url": "https://kartenherz.com/hooks/persolz",
  "description": "Shop-Synchronisation",
  "events": ["contact.created", "contact.unsubscribed"],
  "is_active": true,
  "failure_count": 0,
  "last_status": 200,
  "last_error": null,
  "last_called_at": "2026-10-09T14:05:01+02:00",
  "disabled_at": null,
  "created_at": "2026-10-01T09:00:00+02:00",
  "updated_at": null,
  "secret_hint": "whsec_3f9a…"
}

Das Zustellungs-Objekt

Attribute

Das Zustellungs-Objekt
FeldBeschreibung
id integer Eindeutige ID.
webhook_id integer ID des Webhooks.
event string Name des Ereignisses.
event_id string ID des Ereignisses (evt_…) – bei Wiederholungen gleich, ideal zum Entdoppeln.
status string pending (offen), delivered (zugestellt) oder failed (endgültig fehlgeschlagen).
attempts integer Bisherige Zustellversuche.
response_code integer HTTP-Status Deines Endpunkts oder null.
response_body string Anfang der Antwort Deines Endpunkts.
error string · null Fehler beim letzten Versuch oder null.
duration_ms integer Dauer in Millisekunden.
next_attempt_at string · null Nächster Zustellversuch oder null.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
delivered_at string (Datum/Zeit) Zeitpunkt der erfolgreichen Zustellung oder null.
payload object Gesendeter JSON-Body (Umschlag mit id, event, created_at, data).
payload.id string ID des Ereignisses.
payload.event string Name des Ereignisses.
payload.created_at string (Datum/Zeit) Zeitpunkt des Ereignisses.
payload.data object Daten des Ereignisses.
payload.data.contact Kontakt-Objekt Der Kontakt. Kontakt-Objekt
Das Zustellungs-Objekt JSON
{
  "id": 9001,
  "webhook_id": 4,
  "event": "contact.created",
  "event_id": "evt_3f9a1c2b4d5e6f708192a3b4",
  "status": "delivered",
  "attempts": 1,
  "response_code": 200,
  "response_body": "ok",
  "error": null,
  "duration_ms": 182,
  "next_attempt_at": null,
  "created_at": "2026-10-09T14:05:00+02:00",
  "delivered_at": "2026-10-09T14:05:01+02:00",
  "payload": {
    "id": "evt_3f9a1c2b4d5e6f708192a3b4",
    "event": "contact.created",
    "created_at": "2026-10-09T14:05:00+02:00",
    "data": {
      "contact": {
        "id": 42,
        "email": "anna@beispiel.de",
        "first_name": "Anna",
        "last_name": "Schmidt",
        "address": "Lindenweg 3",
        "zip": "10115",
        "city": "Berlin",
        "phone": "+49 30 1234567",
        "note": "Kam über den Kartenherz-Funnel",
        "tags": ["funnel", "kartenherz"],
        "status": "subscribed",
        "source": "api",
        "custom_fields": {
          "kundennummer": "K-1001"
        },
        "doi_confirmed_at": null,
        "created_at": "2026-10-09T14:05:00+02:00",
        "updated_at": "2026-10-09T14:05:00+02:00",
        "lists": [
          {
            "id": 3,
            "name": "Kartenherz Funnel",
            "color": "#5B3DF5"
          }
        ]
      }
    }
  }
}

Webhooks auflisten

GET /api/v1/webhooks
Scope read

Alle Webhooks mit Zustellstatistik der letzten 7 Tage (ohne Secret).

Antwort

200 OK – Liste von Webhook-Objekten, zusätzlich:

Antwortfelder
FeldBeschreibung
stats_7d object Zustellungen der letzten 7 Tage.
stats_7d.total integer Zustellungen gesamt.
stats_7d.delivered integer Erfolgreich zugestellt.
stats_7d.failed integer Endgültig fehlgeschlagen.
stats_7d.pending integer Noch offen (wartet auf den nächsten Versuch).

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /webhooks
curl https://persolz.com/api/v1/webhooks \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/webhooks",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 4,
      "url": "https://kartenherz.com/hooks/persolz",
      "description": "Shop-Synchronisation",
      "events": ["contact.created", "contact.unsubscribed"],
      "is_active": true,
      "failure_count": 0,
      "last_status": 200,
      "last_error": null,
      "last_called_at": "2026-10-09T14:05:01+02:00",
      "disabled_at": null,
      "created_at": "2026-10-01T09:00:00+02:00",
      "updated_at": null,
      "secret_hint": "whsec_3f9a…",
      "stats_7d": {
        "total": 120,
        "delivered": 119,
        "failed": 1,
        "pending": 0
      }
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Webhook anlegen

POST /api/v1/webhooks
Scope write Idempotency-Key

Legt einen Webhook an. Die Antwort enthält das Signatur-Secret (nur hier und auf der Entwickler-Seite sichtbar). Max. 20 Webhooks.

Request-Body application/json

FeldBeschreibung
url string Pflicht Öffentlich erreichbare http(s)-URL Deines Endpunkts (https empfohlen; keine internen IPs oder localhost).
events array<string> optional Ereignisse oder ["*"] für alle: contact.created, contact.updated, contact.deleted, contact.unsubscribed, list.joined, email.opened, email.clicked, campaign.sent, soap.enrolled, soap.completed, appointment.created, appointment.updated, inbox.received, inbox.replied, inbox.approval_needed, inbox.auto_replied
description string optional Beschreibung.
is_active boolean optional Aktiv (Reaktivieren setzt den Fehlerzähler zurück).

Antwort

201 Angelegt – ein Webhook-Objekt, zusätzlich:

Antwortfelder
FeldBeschreibung
secret string Signatur-Secret zum Prüfen der Webhooks – wird nur beim Anlegen bzw. Erneuern ausgegeben.

Mögliche Fehler: 401 unauthorized 402 plan_required 402 limit_reached 403 forbidden 422 validation_error 429 rate_limited

POST /webhooks
curl -X POST https://persolz.com/api/v1/webhooks \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://kartenherz.com/hooks/persolz",
    "events": ["contact.created", "contact.unsubscribed"],
    "description": "Shop-Synchronisation"
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'url' => 'https://kartenherz.com/hooks/persolz',
        'events' => ['contact.created', 'contact.unsubscribed'],
        'description' => 'Shop-Synchronisation',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://kartenherz.com/hooks/persolz',
    events: ['contact.created', 'contact.unsubscribed'],
    description: 'Shop-Synchronisation'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/webhooks",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "url": "https://kartenherz.com/hooks/persolz",
        "events": ["contact.created", "contact.unsubscribed"],
        "description": "Shop-Synchronisation",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "id": 4,
    "url": "https://kartenherz.com/hooks/persolz",
    "description": "Shop-Synchronisation",
    "events": ["contact.created", "contact.unsubscribed"],
    "is_active": true,
    "failure_count": 0,
    "last_status": 200,
    "last_error": null,
    "last_called_at": "2026-10-09T14:05:01+02:00",
    "disabled_at": null,
    "created_at": "2026-10-01T09:00:00+02:00",
    "updated_at": null,
    "secret_hint": "whsec_3f9a…",
    "secret": "whsec_3f9a1c2b4d5e6f708192a3b4c5d6e7f8091a2b3c"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Webhook abrufen

GET /api/v1/webhooks/{id}
Scope read

Ein Webhook (ohne Secret).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK – ein Webhook-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /webhooks/{id}
curl https://persolz.com/api/v1/webhooks/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks/42');
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks/42', {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/webhooks/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 4,
    "url": "https://kartenherz.com/hooks/persolz",
    "description": "Shop-Synchronisation",
    "events": ["contact.created", "contact.unsubscribed"],
    "is_active": true,
    "failure_count": 0,
    "last_status": 200,
    "last_error": null,
    "last_called_at": "2026-10-09T14:05:01+02:00",
    "disabled_at": null,
    "created_at": "2026-10-01T09:00:00+02:00",
    "updated_at": null,
    "secret_hint": "whsec_3f9a…"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Webhook ändern

PATCH /api/v1/webhooks/{id}
Scope write

Ändert URL, Ereignisse, Beschreibung oder Aktivierung. rotate_secret=true erzeugt ein neues Secret (in der Antwort enthalten).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Request-Body application/json

FeldBeschreibung
url string Pflicht Öffentlich erreichbare http(s)-URL Deines Endpunkts (https empfohlen; keine internen IPs oder localhost).
events array<string> optional Ereignisse oder ["*"] für alle: contact.created, contact.updated, contact.deleted, contact.unsubscribed, list.joined, email.opened, email.clicked, campaign.sent, soap.enrolled, soap.completed, appointment.created, appointment.updated, inbox.received, inbox.replied, inbox.approval_needed, inbox.auto_replied
description string optional Beschreibung.
is_active boolean optional Aktiv (Reaktivieren setzt den Fehlerzähler zurück).
rotate_secret boolean optional Neues Signatur-Secret erzeugen.

Antwort

200 OK – ein Webhook-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 422 validation_error 429 rate_limited

PATCH /webhooks/{id}
curl -X PATCH https://persolz.com/api/v1/webhooks/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["*"],
    "is_active": true
  }'
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'events' => ['*'],
        'is_active' => true,
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks/42', {
  method: 'PATCH',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    events: ['*'],
    is_active: true
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.patch(
    "https://persolz.com/api/v1/webhooks/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    json={
        "events": ["*"],
        "is_active": True,
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "id": 4,
    "url": "https://kartenherz.com/hooks/persolz",
    "description": "Shop-Synchronisation",
    "events": ["contact.created", "contact.unsubscribed"],
    "is_active": true,
    "failure_count": 0,
    "last_status": 200,
    "last_error": null,
    "last_called_at": "2026-10-09T14:05:01+02:00",
    "disabled_at": null,
    "created_at": "2026-10-01T09:00:00+02:00",
    "updated_at": null,
    "secret_hint": "whsec_3f9a…"
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Webhook löschen

DELETE /api/v1/webhooks/{id}
Scope write

Löscht den Webhook und seine Zustellungen.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

204 Gelöscht

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

DELETE /webhooks/{id}
curl -X DELETE https://persolz.com/api/v1/webhooks/42 \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks/42');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status !== 204) {
    $error = json_decode($body, true)['error'] ?? [];
    exit('Fehler ' . $status . ': ' . ($error['message'] ?? $body));
}
echo 'Erledigt.';
const response = await fetch('https://persolz.com/api/v1/webhooks/42', {
  method: 'DELETE',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
if (response.status !== 204) {
  const json = await response.json();
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log('Erledigt.');
import requests

response = requests.delete(
    "https://persolz.com/api/v1/webhooks/42",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
if response.status_code != 204:
    raise SystemExit(f"Fehler {response.status_code}: {response.json()['error']['message']}")
print("Erledigt.")
Antwort 204 Text
HTTP/1.1 204 No Content

(kein Inhalt – der Status allein bestätigt den Erfolg)

Test-Ping senden

POST /api/v1/webhooks/{id}/test
Scope write Idempotency-Key

Sendet sofort ein Ereignis „test.ping“ an den Webhook und liefert das Ergebnis der Zustellung.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Antwort

200 OK

Antwortfelder
FeldBeschreibung
delivered boolean true, wenn Dein Endpunkt mit 2xx geantwortet hat.
status integer HTTP-Status Deines Endpunkts oder null.
error string · null Fehlermeldung oder null.
delivery Zustellungs-Objekt Die Zustellung. Zustellungs-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

POST /webhooks/{id}/test
curl -X POST https://persolz.com/api/v1/webhooks/42/test \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks/42/test');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks/42/test', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/webhooks/42/test",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "delivered": true,
    "status": 200,
    "error": null,
    "delivery": {
      "event": "test.ping",
      "status": "delivered",
      "id": 9001,
      "webhook_id": 4,
      "event_id": "evt_3f9a1c2b4d5e6f708192a3b4",
      "attempts": 1,
      "response_code": 200,
      "response_body": "ok",
      "error": null,
      "duration_ms": 182,
      "next_attempt_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "delivered_at": "2026-10-09T14:05:01+02:00",
      "payload": {
        "id": "evt_3f9a1c2b4d5e6f708192a3b4",
        "event": "contact.created",
        "created_at": "2026-10-09T14:05:00+02:00",
        "data": {
          "contact": {
            "id": 42,
            "email": "anna@beispiel.de",
            "first_name": "Anna",
            "last_name": "Schmidt",
            "address": "Lindenweg 3",
            "zip": "10115",
            "city": "Berlin",
            "phone": "+49 30 1234567",
            "note": "Kam über den Kartenherz-Funnel",
            "tags": ["funnel", "kartenherz"],
            "status": "subscribed",
            "source": "api",
            "custom_fields": {
              "kundennummer": "K-1001"
            },
            "doi_confirmed_at": null,
            "created_at": "2026-10-09T14:05:00+02:00",
            "updated_at": "2026-10-09T14:05:00+02:00",
            "lists": [
              {
                "id": 3,
                "name": "Kartenherz Funnel",
                "color": "#5B3DF5"
              }
            ]
          }
        }
      }
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Zustellungen

GET /api/v1/webhooks/{id}/deliveries
Scope read paginiert

Zustellprotokoll (neueste zuerst) mit Payload, Antwortcode, Antwort-Auszug und nächstem Versuch. Aufbewahrung 30 Tage.

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource

Query-Parameter

NameBeschreibung
status string optional pending | delivered | failed Werte: pending, delivered, failed Beispiel: failed
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Zustellungs-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

GET /webhooks/{id}/deliveries
curl "https://persolz.com/api/v1/webhooks/42/deliveries?status=failed" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'failed',
]);
$ch = curl_init('https://persolz.com/api/v1/webhooks/42/deliveries?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'failed'
});
const response = await fetch('https://persolz.com/api/v1/webhooks/42/deliveries?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/webhooks/42/deliveries",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "failed",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 9001,
      "webhook_id": 4,
      "event": "contact.created",
      "event_id": "evt_3f9a1c2b4d5e6f708192a3b4",
      "status": "delivered",
      "attempts": 1,
      "response_code": 200,
      "response_body": "ok",
      "error": null,
      "duration_ms": 182,
      "next_attempt_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "delivered_at": "2026-10-09T14:05:01+02:00",
      "payload": {
        "id": "evt_3f9a1c2b4d5e6f708192a3b4",
        "event": "contact.created",
        "created_at": "2026-10-09T14:05:00+02:00",
        "data": {
          "contact": {
            "id": 42,
            "email": "anna@beispiel.de",
            "first_name": "Anna",
            "last_name": "Schmidt",
            "address": "Lindenweg 3",
            "zip": "10115",
            "city": "Berlin",
            "phone": "+49 30 1234567",
            "note": "Kam über den Kartenherz-Funnel",
            "tags": ["funnel", "kartenherz"],
            "status": "subscribed",
            "source": "api",
            "custom_fields": {
              "kundennummer": "K-1001"
            },
            "doi_confirmed_at": null,
            "created_at": "2026-10-09T14:05:00+02:00",
            "updated_at": "2026-10-09T14:05:00+02:00",
            "lists": [
              {
                "id": 3,
                "name": "Kartenherz Funnel",
                "color": "#5B3DF5"
              }
            ]
          }
        }
      }
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

Zustellung erneut senden

POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/resend
Scope write Idempotency-Key

Sendet eine Zustellung sofort noch einmal (gleiche Event-ID, neue Signatur).

Pfad-Parameter

NameBeschreibung
id integer Pflicht ID der Ressource
deliveryId integer Pflicht ID der Zustellung

Antwort

200 OK

Antwortfelder
FeldBeschreibung
delivered boolean true, wenn Dein Endpunkt mit 2xx geantwortet hat.
status integer HTTP-Status Deines Endpunkts oder null.
error string · null Fehlermeldung oder null.
delivery Zustellungs-Objekt Die Zustellung. Zustellungs-Objekt

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 404 not_found 429 rate_limited

POST /webhooks/{id}/deliveries/{deliveryId}/resend
curl -X POST https://persolz.com/api/v1/webhooks/42/deliveries/9001/resend \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$ch = curl_init('https://persolz.com/api/v1/webhooks/42/deliveries/9001/resend');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/webhooks/42/deliveries/9001/resend', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/webhooks/42/deliveries/9001/resend",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "delivered": true,
    "status": 200,
    "error": null,
    "delivery": {
      "id": 9001,
      "webhook_id": 4,
      "event": "contact.created",
      "event_id": "evt_3f9a1c2b4d5e6f708192a3b4",
      "status": "delivered",
      "attempts": 1,
      "response_code": 200,
      "response_body": "ok",
      "error": null,
      "duration_ms": 182,
      "next_attempt_at": null,
      "created_at": "2026-10-09T14:05:00+02:00",
      "delivered_at": "2026-10-09T14:05:01+02:00",
      "payload": {
        "id": "evt_3f9a1c2b4d5e6f708192a3b4",
        "event": "contact.created",
        "created_at": "2026-10-09T14:05:00+02:00",
        "data": {
          "contact": {
            "id": 42,
            "email": "anna@beispiel.de",
            "first_name": "Anna",
            "last_name": "Schmidt",
            "address": "Lindenweg 3",
            "zip": "10115",
            "city": "Berlin",
            "phone": "+49 30 1234567",
            "note": "Kam über den Kartenherz-Funnel",
            "tags": ["funnel", "kartenherz"],
            "status": "subscribed",
            "source": "api",
            "custom_fields": {
              "kundennummer": "K-1001"
            },
            "doi_confirmed_at": null,
            "created_at": "2026-10-09T14:05:00+02:00",
            "updated_at": "2026-10-09T14:05:00+02:00",
            "lists": [
              {
                "id": 3,
                "name": "Kartenherz Funnel",
                "color": "#5B3DF5"
              }
            ]
          }
        }
      }
    }
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

API-Referenz

Protokolle

Aktivitätsprotokoll und API-Protokoll Deines Kontos.

Objekte: Protokoll-Objekt · API-Protokoll-Objekt

Endpunkte2

Das Protokoll-Objekt

Attribute

Das Protokoll-Objekt
FeldBeschreibung
id integer Eindeutige ID.
level string Stufe: debug, info, success, warning, error oder critical.
channel string Bereich, z. B. email, newsletter, soap, contacts, smtp, api.
message string Meldung.
context object Zusatzdaten zum Eintrag (Geheimnisse maskiert).
context.campaign_id integer Betroffene Kampagne.
context.sent integer Versendete Mails.
ip string · null IP-Adresse.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
Das Protokoll-Objekt JSON
{
  "id": 1201,
  "level": "success",
  "channel": "newsletter",
  "message": "Newsletter versendet: „Herbst-Aktion“",
  "context": {
    "campaign_id": 812,
    "sent": 1248
  },
  "ip": null,
  "created_at": "2026-10-09T09:04:31+02:00"
}

Das API-Protokoll-Objekt

Attribute

Das API-Protokoll-Objekt
FeldBeschreibung
id integer Eindeutige ID.
method string HTTP-Methode.
endpoint string Aufgerufener Pfad.
route string Erkannte Route (Muster, z. B. /api/v1/contacts/{id}) oder null.
status_code integer HTTP-Status der Antwort.
duration_ms integer Dauer in Millisekunden.
ip string IP-Adresse.
origin string Origin-Header (bei Aufrufen aus dem Browser) oder null.
user_agent string User-Agent der Anfrage.
error string · null Fehlermeldung bei Status ≥ 400, sonst null.
request_id string Request-ID – gib sie bei Support-Anfragen an.
key object Verwendeter API-Schlüssel.
key.id integer ID des Schlüssels.
key.name string Name des Schlüssels.
key.prefix string Anfang des Schlüssels.
created_at string (Datum/Zeit) Zeitpunkt der Anlage (ISO 8601).
Das API-Protokoll-Objekt JSON
{
  "id": 3301,
  "method": "POST",
  "endpoint": "/api/v1/contacts",
  "route": "/api/v1/contacts",
  "status_code": 201,
  "duration_ms": 48,
  "ip": "203.0.113.7",
  "origin": "https://kartenherz.com",
  "user_agent": "Mozilla/5.0",
  "error": null,
  "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
  "key": {
    "id": 5,
    "name": "Funnel kartenherz.com",
    "prefix": "pz_live_AbCdEf"
  },
  "created_at": "2026-10-09T14:05:00+02:00"
}

Aktivitätsprotokoll

GET /api/v1/logs
Scope read paginiert

Das Protokoll Deines Kontos (Versand, Kontakte, Automationen, Fehler …), neueste zuerst. Geheimnisse sind maskiert.

Query-Parameter

NameBeschreibung
level string optional debug | info | success | warning | error | critical | problems (= ab warning) Beispiel: problems
channel string optional Bereich, z. B. email, newsletter, soap, contacts, smtp, api Beispiel: smtp
q string optional Volltextsuche. Beispiel: zustellung
from string optional Ab Datum YYYY-MM-DD. Beispiel: 2026-10-01
to string optional Bis Datum YYYY-MM-DD. Beispiel: 2026-10-09
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von Protokoll-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /logs
curl "https://persolz.com/api/v1/logs?level=problems" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'level' => 'problems',
]);
$ch = curl_init('https://persolz.com/api/v1/logs?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  level: 'problems'
});
const response = await fetch('https://persolz.com/api/v1/logs?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/logs",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "level": "problems",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 1201,
      "level": "success",
      "channel": "newsletter",
      "message": "Newsletter versendet: „Herbst-Aktion“",
      "context": {
        "campaign_id": 812,
        "sent": 1248
      },
      "ip": null,
      "created_at": "2026-10-09T09:04:31+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

API-Protokoll

GET /api/v1/api-logs
Scope read paginiert

Deine API-Anfragen (aller Schlüssel) mit Status, Dauer, Herkunft und Fehlermeldung. Aufbewahrung 90 Tage.

Query-Parameter

NameBeschreibung
status string optional success (2xx/3xx) | error (≥ 400) oder ein exakter Code wie 422 Beispiel: error
method string optional GET | POST | PUT | PATCH | DELETE Beispiel: POST
key_id integer optional Nur Anfragen dieses Schlüssels. Beispiel: 5
q string optional Suche in Pfad, IP, Fehler. Beispiel: contacts
from string optional Ab Datum. Beispiel: 2026-10-01
to string optional Bis Datum. Beispiel: 2026-10-09
page integer optional Seite (ab 1). Standard: 1
per_page integer optional Einträge pro Seite (1–100). Standard: 25

Antwort

200 OK – Liste von API-Protokoll-Objekten

Mögliche Fehler: 401 unauthorized 402 plan_required 403 forbidden 429 rate_limited

GET /api-logs
curl "https://persolz.com/api/v1/api-logs?status=error" \
  -H "Authorization: Bearer pz_live_DEIN_API_SCHLUESSEL"
<?php
$apiKey = 'pz_live_DEIN_API_SCHLUESSEL'; // besser: getenv('PERSOLZ_API_KEY')

$query = http_build_query([
    'status' => 'error',
]);
$ch = curl_init('https://persolz.com/api/v1/api-logs?' . $query);
curl_setopt_array($ch, [
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const params = new URLSearchParams({
  status: 'error'
});
const response = await fetch('https://persolz.com/api/v1/api-logs?' + params, {
  headers: {
    'Authorization': 'Bearer pz_live_DEIN_API_SCHLUESSEL'
  }
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.get(
    "https://persolz.com/api/v1/api-logs",
    headers={
        "Authorization": "Bearer pz_live_DEIN_API_SCHLUESSEL",
    },
    params={
        "status": "error",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": [
    {
      "id": 3301,
      "method": "POST",
      "endpoint": "/api/v1/contacts",
      "route": "/api/v1/contacts",
      "status_code": 201,
      "duration_ms": 48,
      "ip": "203.0.113.7",
      "origin": "https://kartenherz.com",
      "user_agent": "Mozilla/5.0",
      "error": null,
      "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
      "key": {
        "id": 5,
        "name": "Funnel kartenherz.com",
        "prefix": "pz_live_AbCdEf"
      },
      "created_at": "2026-10-09T14:05:00+02:00"
    }
  ],
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e",
    "pagination": {
      "page": 1,
      "per_page": 25,
      "total": 1,
      "pages": 1
    }
  }
}

API-Referenz

Öffentliche Anmeldung

Anmeldeformulare (z. B. aus Deinem Funnel) – ohne API-Schlüssel.

Diese Endpunkte brauchen keinen API-Schlüssel und sind für Formulare auf Deiner Website gedacht. Anleitung: Funnel-Integration.

Öffentliche Anmeldung (ohne Schlüssel)

POST /api/v1/subscribe/{token}
Öffentlich

Anmeldung an eine Liste über deren öffentlichen Token – für Formulare, Landingpages und Funnels (CORS „*“). Akzeptiert JSON oder Formulardaten. Double-Opt-in der Liste wird beachtet (Status pending + Bestätigungsmail). Spamschutz: Das Honeypot-Feld „website“ muss leer bleiben; max. 10 Anmeldungen pro Minute je IP und 200 pro Tag je Liste. HTML-Formulare (Accept: text/html oder mit success_url) werden per 303 auf success_url weitergeleitet (bei Fehlern auf error_url, sonst eine kleine Danke-/Fehlerseite).

Pfad-Parameter

NameBeschreibung
token string Pflicht Öffentlicher Token der Liste (32 Zeichen, siehe GET /lists).

Request-Body application/json

FeldBeschreibung
email string Pflicht E-Mail-Adresse.
first_name string optional Vorname.
last_name string optional Nachname.
phone string optional Telefon.
address string optional Adresse.
zip string optional PLZ.
city string optional Ort.
note string optional Notiz (max. 1000 Zeichen).
tags array<string> optional Tags (Array oder kommagetrennt, max. 10).
website string optional Honeypot – muss leer bleiben.
success_url string optional Weiterleitung nach Erfolg (http/https). Hat die Liste „Erlaubte Weiterleitungs-Domains“, werden andere Hosts ignoriert (→ Persolz-Danke-Seite).
error_url string optional Weiterleitung bei Fehlern (?error=… wird angehängt) – nur bei gültigem Listen-Token und erlaubter Domain.

Antwort

201 Angemeldet (200 = bereits bekannt / aktualisiert)

Antwortfelder
FeldBeschreibung
state string subscribed (angemeldet) oder pending (Bestätigung per E-Mail nötig).
double_optin boolean Die Liste verlangt eine Bestätigung per E-Mail.
message string Verständliche Meldung auf Deutsch.
redirect_url string · null Weiterleitungsziel der Liste nach der Anmeldung oder null.

Mögliche Fehler: 403 forbidden 404 not_found 422 validation_error 429 rate_limited

POST /subscribe/{token}
curl -X POST https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c \
  -H "Content-Type: application/json" \
  -d '{
    "email": "anna@beispiel.de",
    "first_name": "Anna",
    "tags": ["funnel"],
    "website": ""
  }'
<?php
$ch = curl_init('https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'email' => 'anna@beispiel.de',
        'first_name' => 'Anna',
        'tags' => ['funnel'],
        'website' => '',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    email: 'anna@beispiel.de',
    first_name: 'Anna',
    tags: ['funnel'],
    website: ''
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c",
    json={
        "email": "anna@beispiel.de",
        "first_name": "Anna",
        "tags": ["funnel"],
        "website": "",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 201 JSON
{
  "data": {
    "state": "pending",
    "double_optin": true,
    "message": "Fast geschafft! Wir haben Dir eine E-Mail geschickt – bitte bestätige Deine Anmeldung über den Link darin.",
    "redirect_url": null
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Einbettbares Anmeldeformular

GET /api/v1/subscribe/{token}/form
Öffentlich

Ein schlichtes, responsives HTML-Anmeldeformular für die Liste – zum Verlinken oder per <iframe> einbetten. Optionen per Query: fields (z. B. first_name,last_name), button, title, success_url, theme (light|dark), accent (Hex ohne #).

Pfad-Parameter

NameBeschreibung
token string Pflicht Öffentlicher Token der Liste.

Query-Parameter

NameBeschreibung
fields string optional Zusätzliche Felder: first_name, last_name, phone, city … Beispiel: first_name
button string optional Button-Text. Beispiel: Jetzt anmelden
title string optional Überschrift (leer = keine). Beispiel: Newsletter
success_url string optional Weiterleitung nach Erfolg. Beispiel: https://kartenherz.com/danke
theme string optional light | dark Beispiel: light
accent string optional Akzentfarbe als Hex ohne #. Beispiel: 5B3DF5

Antwort

200 HTML-Seite (text/html)

Mögliche Fehler: 404 not_found

GET /subscribe/{token}/form
curl "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form?fields=first_name&success_url=https%3A%2F%2Fkartenherz.com%2Fdanke"
<?php
$query = http_build_query([
    'fields' => 'first_name',
    'success_url' => 'https://kartenherz.com/danke',
]);
$ch = curl_init('https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form?' . $query);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

echo $status === 200 ? $body : 'Fehler ' . $status;
const params = new URLSearchParams({
  fields: 'first_name',
  success_url: 'https://kartenherz.com/danke'
});
const response = await fetch('https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form?' + params);
console.log(await response.text());
import requests

response = requests.get(
    "https://persolz.com/api/v1/subscribe/9f2c4e6a8b0d1f3e5a7c9e1b3d5f7a9c/form",
    params={
        "fields": "first_name",
        "success_url": "https://kartenherz.com/danke",
    },
    timeout=30,
)
print(response.text)
Antwort 200 HTML
<!doctype html><html lang="de">…</html>

API-Referenz

Integrationen

Zahlungsanbieter (Digistore24, CopeCart) übertragen Käufer automatisch in Persolz.

Die Adressen mit Deinem persönlichen Integrations-Token findest Du unter API & Doku → Integrationen. Dort legst Du auch Liste, Seifenoper, Tags und das Secret fest.

Digistore24 IPN

POST /api/v1/integrations/digistore24/{token}
Öffentlich

Empfänger für die Digistore24-IPN (Instant Payment Notification). Trage die URL mit Deinem persönlichen Integrations-Token (siehe „API & Doku“ → Integrationen) in Digistore24 ein. Ist eine IPN-Passphrase hinterlegt, wird die Signatur „sha_sign“ geprüft. Zahlungsereignisse (on_payment, on_rebill_resumed) übernehmen den Käufer als Kontakt in die gewählte Liste (optional Start einer Seifenoper), on_refund / on_chargeback / on_payment_missed / on_rebill_cancelled setzen Tags. Antwort: „OK“ (text/plain).

Pfad-Parameter

NameBeschreibung
token string Pflicht Dein persönlicher Integrations-Token.

Request-Body application/x-www-form-urlencoded

FeldBeschreibung
event string Pflicht on_payment, on_refund, on_chargeback, on_payment_missed, on_rebill_resumed, on_rebill_cancelled, connection_test …
email string Pflicht E-Mail des Käufers.
address_first_name string optional Vorname.
address_last_name string optional Nachname.
product_id string optional Produkt-ID.
product_name string optional Produktname.
order_id string optional Bestell-ID.
sha_sign string optional Signatur (SHA-512 über die sortierten Parameter + Passphrase).

Antwort

200 text/plain „OK“

Mögliche Fehler: 403 forbidden 404 not_found 422 validation_error

POST /integrations/digistore24/{token}
curl -X POST https://persolz.com/api/v1/integrations/digistore24/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a \
  --data-urlencode "event=on_payment" \
  --data-urlencode "email=anna@beispiel.de" \
  --data-urlencode "address_first_name=Anna" \
  --data-urlencode "address_last_name=Schmidt" \
  --data-urlencode "product_id=12345" \
  --data-urlencode "product_name=Kartenset-Abo" \
  --data-urlencode "order_id=ABCD1234"
<?php
$ch = curl_init('https://persolz.com/api/v1/integrations/digistore24/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_POSTFIELDS     => http_build_query([
        'event' => 'on_payment',
        'email' => 'anna@beispiel.de',
        'address_first_name' => 'Anna',
        'address_last_name' => 'Schmidt',
        'product_id' => '12345',
        'product_name' => 'Kartenset-Abo',
        'order_id' => 'ABCD1234',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

echo $status === 200 ? $body : 'Fehler ' . $status;
const response = await fetch('https://persolz.com/api/v1/integrations/digistore24/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a', {
  method: 'POST',
  body: new URLSearchParams({
    event: 'on_payment',
    email: 'anna@beispiel.de',
    address_first_name: 'Anna',
    address_last_name: 'Schmidt',
    product_id: '12345',
    product_name: 'Kartenset-Abo',
    order_id: 'ABCD1234'
  })
});
console.log(await response.text());
import requests

response = requests.post(
    "https://persolz.com/api/v1/integrations/digistore24/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a",
    data={
        "event": "on_payment",
        "email": "anna@beispiel.de",
        "address_first_name": "Anna",
        "address_last_name": "Schmidt",
        "product_id": "12345",
        "product_name": "Kartenset-Abo",
        "order_id": "ABCD1234",
    },
    timeout=30,
)
print(response.text)
Antwort 200 Text
OK

CopeCart IPN

POST /api/v1/integrations/copecart/{token}
Öffentlich

Empfänger für CopeCart-IPN (JSON). Ist ein IPN-Secret hinterlegt, wird der Header „X-Copecart-Signature“ (Base64-HMAC-SHA256 über den Body) geprüft. payment.made / payment.recurring übernehmen den Käufer als Kontakt (Liste + optional Seifenoper), payment.refunded / payment.charged_back / payment.failed / payment.recurring.cancelled setzen Tags.

Pfad-Parameter

NameBeschreibung
token string Pflicht Dein persönlicher Integrations-Token.

Header

NameBeschreibung
X-Copecart-Signature string optional Base64(HMAC-SHA256(Body, Secret)) – Pflicht, wenn ein Secret hinterlegt ist. Beispiel: k1R0…=

Request-Body application/json

FeldBeschreibung
event_type string Pflicht payment.made, payment.refunded, payment.charged_back, payment.failed, payment.recurring, payment.recurring.cancelled …
buyer_email string Pflicht E-Mail des Käufers.
buyer_firstname string optional Vorname.
buyer_lastname string optional Nachname.
product_id string optional Produkt-ID.
product_name string optional Produktname.
order_id string optional Bestell-ID.

Antwort

200 OK

Antwortfelder
FeldBeschreibung
received boolean Die Zahlungsbenachrichtigung wurde angenommen.
action string Was passiert ist: contact_upserted, tagged, ignored …
contact_id integer ID des angelegten bzw. aktualisierten Kontakts.

Mögliche Fehler: 403 forbidden 404 not_found 422 validation_error

POST /integrations/copecart/{token}
curl -X POST https://persolz.com/api/v1/integrations/copecart/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a \
  -H "X-Copecart-Signature: k1R0...=" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "payment.made",
    "buyer_email": "anna@beispiel.de",
    "buyer_firstname": "Anna",
    "buyer_lastname": "Schmidt",
    "product_name": "Kartenset-Abo",
    "order_id": "CC-4711"
  }'
<?php
$ch = curl_init('https://persolz.com/api/v1/integrations/copecart/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a');
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => 'POST',
    CURLOPT_HTTPHEADER     => [
        'X-Copecart-Signature: k1R0...=',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'event_type' => 'payment.made',
        'buyer_email' => 'anna@beispiel.de',
        'buyer_firstname' => 'Anna',
        'buyer_lastname' => 'Schmidt',
        'product_name' => 'Kartenset-Abo',
        'order_id' => 'CC-4711',
    ]),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
]);
$body = (string) curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$json = json_decode($body, true);
if ($status >= 400) {
    exit('Fehler ' . $status . ': ' . ($json['error']['message'] ?? $body));
}
print_r($json['data']);
const response = await fetch('https://persolz.com/api/v1/integrations/copecart/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a', {
  method: 'POST',
  headers: {
    'X-Copecart-Signature': 'k1R0...=',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    event_type: 'payment.made',
    buyer_email: 'anna@beispiel.de',
    buyer_firstname: 'Anna',
    buyer_lastname: 'Schmidt',
    product_name: 'Kartenset-Abo',
    order_id: 'CC-4711'
  })
});
const json = await response.json();
if (!response.ok) {
  throw new Error(`Fehler ${response.status}: ${json.error.message}`);
}
console.log(json.data);
import requests

response = requests.post(
    "https://persolz.com/api/v1/integrations/copecart/0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a",
    headers={
        "X-Copecart-Signature": "k1R0...=",
    },
    json={
        "event_type": "payment.made",
        "buyer_email": "anna@beispiel.de",
        "buyer_firstname": "Anna",
        "buyer_lastname": "Schmidt",
        "product_name": "Kartenset-Abo",
        "order_id": "CC-4711",
    },
    timeout=30,
)
result = response.json()
if not response.ok:
    raise SystemExit(f"Fehler {response.status_code}: {result['error']['message']}")
print(result["data"])
Antwort 200 JSON
{
  "data": {
    "received": true,
    "action": "contact_upserted",
    "contact_id": 42
  },
  "meta": {
    "request_id": "req_2b9f1c7d0e3a4b5c6d7e"
  }
}

Ausprobieren

API-Konsole

Probiere jeden Endpunkt direkt hier aus – mit Deinem eigenen Schlüssel, vorausgefüllten Beispielen und der echten Antwort inklusive Status und Headern.

Melde Dich an, um die Konsole zu nutzen. Mit einem Pro-Konto probierst Du jeden Endpunkt mit Deinem eigenen Schlüssel aus – ganz ohne Code.

Anmelden

Werkzeuge

OpenAPI & Markdown

Die komplette API gibt es auch maschinenlesbar – für Postman, Insomnia, Code-Generatoren oder Deinen KI-Assistenten.

Downloads

  • OpenAPI 3.1 (/api/v1/openapi.json): alle Endpunkte, Parameter, Beispiele, Fehler und die Webhook-Ereignisse. In Postman: Import → Link und die URL einfügen.
  • Markdown (/api-docs/markdown): diese Dokumentation als eine Datei – praktisch zum Offline-Lesen oder um sie einem KI-Assistenten als Kontext zu geben.
Per Kommandozeile Shell
# OpenAPI 3.1 (z. B. für Postman: Import → Link)
curl -o persolz-openapi.json https://persolz.com/api/v1/openapi.json

# Diese Dokumentation als Markdown (z. B. für Deinen KI-Assistenten)
curl -o persolz-api.md "https://persolz.com/api-docs/markdown"

Neuigkeiten

Changelog

Neue Endpunkte und Felder kommen ohne Vorankündigung dazu – bestehende Felder und Endpunkte ändern sich innerhalb von /api/v1 nicht inkompatibel. Größere Umbrüche gäbe es nur in einer neuen Version (/api/v2).

v1.1.0
  • Neue Gruppe Posteingang mit 16 Endpunkten: Postfächer und Abruf, Mails mit Filtern (Ansicht, Kategorie, Suche, Kontakt, since) und Inhalt, Massenaktionen, Antworten, Entwürfe (senden, planen, stoppen), KI-Entwürfe, Freigaben und Kennzahlen.
  • Webhook-Ereignisse inbox.received, inbox.replied, inbox.approval_needed und inbox.auto_replied.
v1.0.0
  • Erste öffentliche Version der REST-API v1 mit 111 Endpunkten: Kontakte, Listen, Abmeldungen, E-Mails, Newsletter, Seinfeld-Mails, Seifenopern, Vorlagen, Anhänge, SMTP, Kampagnen & Analytics, A/B-Tests, Termine, KI, Webhooks und Protokolle.
  • Webhooks mit 13 Ereignissen, HMAC-SHA256-Signatur, automatischen Wiederholungen und Zustellprotokoll.
  • Idempotency-Key für POST, CORS mit Origin-Freigaben, IP-Freigaben und Rate-Limits je Schlüssel.
  • Öffentliche Anmelde-Endpunkte für Formulare sowie Integrationen für Digistore24 und CopeCart.
  • OpenAPI-3.1-Beschreibung, API-Konsole und diese Dokumentation (auch als Markdown).

Persolz REST-API v1.1.0 · 127 Endpunkte · OpenAPI 3.1 · Markdown · Fragen? Schreib uns.