# NewsletterKit API > Interne Middleware zwischen Kundenplattformen und Postmark. Verschickt wird > immer ein Template — im Dashboard gebaut oder aus dem Builder des Portals > gespeichert. Transaktional einzeln und synchron, Newsletter als Versandjob. > Jede Mail hat eine ID; Auswertung und Sperren liegen bei NewsletterKit, das > Portal fragt ab. Die Einwilligung liegt im CRM des Kunden. Der API-Key > bestimmt den Kunden; der Postmark-Server-Token verlässt nie den Server. Base URL: https://api.newsletterkit.ventureon.io/v1 Docs: https://docs.newsletterkit.ventureon.io Auth: Authorization: Bearer (Scopes: full | reporting) Version: v1 ## Standard-Header - Authorization (immer): Bearer . Bestimmt Kunde + Plan. Niemals clientseitig im Browser verwenden. - Content-Type (bei JSON-Body): application/json — für alle POST/PUT-Requests mit JSON-Body. - Idempotency-Key (empfohlen (POST/PUT)): Eindeutige UUID pro Erstellung/Versand. Wiederholte Requests mit gleichem Key erzeugen keine Dubletten (kein doppelter Versand). - NewsletterKit-Version (optional): Pinnt die API-Version (z. B. v1). Ohne Header gilt die Account-Default-Version. - X-Request-Id (optional): Eigene Korrelations-ID; wird in Logs & Support-Anfragen zurückgegeben. ## Fehlercodes - 400 bad_request: Request-Format ungültig (JSON-Syntax, fehlende Felder). - 401 unauthorized: API-Key fehlt, ungültig oder widerrufen. - 403 forbidden: Key-Scope deckt diese Operation nicht (z. B. reporting-only bei schreibenden Calls). - 404 not_found: Ressource existiert nicht oder gehört nicht zum Kunden (falsche ID). - 409 conflict: Idempotenz-Konflikt oder Statuskollision. - 422 validation_error: Felder vorhanden, aber inhaltlich ungültig (siehe details[]). - 429 rate_limited: Zu viele Requests → Retry-After beachten. - 429 quota_exceeded: Monatslimit oder Höchstzahl je Job erreicht — eine Sicherung des Kunden, im Admin anhebbar. - 5xx internal_error: Fehler bei uns oder bei Postmark. Mit Backoff erneut versuchen. ## Integrationspfad — die fünf Abläufe ### Transaktionale Mail Ein fertiges Template des Kunden, ein Empfänger, sofort. Die Antwort trägt unsere Mail-ID — sie gehört an die Bestellung oder den Nutzer, damit das Portal später den Status nachfragen kann. 1. GET /v1/templates — Template der Art transaktional wählen, params lesen (name, label, type, required, example). 2. POST /v1/templates/{id}/send mit to und params; Idempotency-Key gegen doppelten Versand bei Retries. 3. Bei Bedarf GET /v1/messages/{id}: zugestellt oder gebounct, mit Grund. ```ts // Server Action / Route Handler import { newsletterkit } from "@/lib/newsletterkit"; const { data } = await newsletterkit<{ data: { message_id: string; status: string } }>( "/templates/00000000-0000-4000-8000-0000000000f3/send", { method: "POST", headers: { "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ to: "mira@example.com", params: { titel: "Dein Zugang zum Dashboard", name: "Mira", text: "Du bist als Redaktion eingetragen.", link: "https://kunde.de/einladung?token=…", screenshot: "https://kunde.de/bilder/dashboard.jpg", }, }), }, ); // data.status: accepted | suppressed | invalid — data.message_id am Nutzer speichern await db.user.update({ where: { id: userId }, data: { inviteMailId: data.message_id } }); ``` ### Newsletter aus fertigem Template Das Portal zeigt die Newsletter-Templates des Kunden, der Nutzer füllt die Parameter (eine KI kann sie aus Bezeichnung und Beispiel vorausfüllen), wählt Empfänger aus dem CRM und schickt ab. Verschickt wird als Job. 1. GET /v1/templates — Art newsletter, params lesen. 2. POST /v1/render/preview zum Prüfen — oder GET /v1/templates/{id} und die Blöcke mit den Werten rendern lassen. 3. POST /v1/send-jobs mit template_id, params, optional scheduled_at. 4. POST /v1/send-jobs/{id}/recipients — Kontakte aus dem CRM, gefiltert nach Einwilligung UND keiner Sperre, in Blöcken bis 5.000, je Empfänger data für {{platzhalter}}. 5. POST /v1/send-jobs/{id}/start, danach GET /v1/send-jobs/{id} alle paar Sekunden, bis status completed ist. ```ts import { newsletterkit } from "@/lib/newsletterkit"; // 1) Job anlegen const job = await newsletterkit<{ data: { id: string } }>("/send-jobs", { method: "POST", headers: { "Idempotency-Key": crypto.randomUUID() }, body: JSON.stringify({ template_id: templateId, params: { titel: "Sommerprogramm" } }), }); // 2) Empfänger aus dem CRM: Einwilligung UND keine Sperre — nie nur eins davon const contacts = await db.contact.findMany({ where: { marketingConsent: true, emailSuppressed: false }, select: { id: true, email: true, firstName: true }, }); for (let i = 0; i < contacts.length; i += 5000) { const chunk = contacts.slice(i, i + 5000); const res = await newsletterkit<{ data: { recipients: { email: string; message_id: string }[] } }>( `/send-jobs/${job.data.id}/recipients`, { method: "POST", headers: { "Idempotency-Key": `${job.data.id}:${i}` }, body: JSON.stringify({ recipients: chunk.map((c) => ({ email: c.email, data: { vorname: c.firstName } })), }), }, ); // Mail-IDs an die Kontakte schreiben — der Schlüssel für Status und Auswertung for (const r of res.data.recipients) await db.newsletterMail.create({ data: { contactEmail: r.email, messageId: r.message_id, jobId: job.data.id } }); } // 3) Starten und abwarten await newsletterkit(`/send-jobs/${job.data.id}/start`, { method: "POST" }); ``` ### Newsletter aus dem Builder Der Nutzer stapelt Blöcke im Canvas des Portals und füllt sie mit Inhalt. Das Portal rendert nie selbst: Jede Vorschau und jede Kachel kommt fertig aus NewsletterKit, mit den Design-Regeln des Kunden dieses Moments. Am Ende wird der Stapel als eigenes Template gespeichert und wie ein fertiges verschickt. 1. GET /v1/blocks — Blöcke mit Parameter-Schema und tile_url; Kacheln über GET /v1/blocks/{id}/tile in iframes mit srcdoc. 2. Bei jeder Änderung (entprellt, ~300 ms): POST /v1/render/preview mit dem Stapel — HTML in ein iframe. 3. Bilder: POST /v1/assets → PUT an upload.url → POST /v1/assets/{id}/complete; die url in Bild-Parameter setzen. 4. POST /v1/templates speichert den Stapel als Template (Art newsletter; kein Footer im Stapel, der kommt aus dem Rahmen des Kunden). 5. Weiter wie beim fertigen Template: Job anlegen, Empfänger anhängen, starten. ```ts import { newsletterkit } from "@/lib/newsletterkit"; // Live-Vorschau: der Stapel des Builders, unverändert, an die API export async function preview(blocks: { block_id: string; values: Record }[]) { const { data } = await newsletterkit<{ data: { html: string | null; errors: string[] } }>( "/render/preview", { method: "POST", body: JSON.stringify({ blocks }) }, ); return data.html; //