Angebotsgenerator (Market-Integration)

Zuletzt aktualisiert: Aug 31, 2026Abschnitt: Integrationen

Vollständige API-Referenz: https://api.mailoo.app/docs/v1

Der Angebotsgenerator ist eine Market-exklusive Funktion: Assistenten sind auf eine MARKET-Integration begrenzt, damit sie veröffentlichte Produkte und Preislisten aus demselben Katalog auflösen können. Angebotsspeicherung, SMTP und ausgehende Zustellung sind eigenständig (separate Tabellen und Mail-Einstellungen von anderen Integrationstypen).

Für Katalog-Leseendpunkte (Typen, Produkte, Preislisten) verwenden Sie weiterhin ``market.external-read`` wie in market-catalog-external-api{.interpreted-text role="doc"} dokumentiert.

Übersicht

  • Dashboard: Projekt → Market-Integration → Reiter Offer generator --- Generatoren auflisten, erstellen, bearbeiten, löschen; Assistenten-JSON, optionale KI und SMTP konfigurieren; externe URL-Muster für pre-form.
  • Eigentümer-API (JWT / Session-Bearer): Generatoren und Mail-Einstellungen unter /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators… erstellen und aktualisieren.
  • Externe API (``X-API-Key``): Gleicher Pfad-Präfix wie Market-externe Routen --- /api/v1/market/{projectUid}/integrations/{integrationId}/offer-generators/… --- mit separaten Berechtigungen von market.external-read.
  • Berechtigungen:
  • ``offer-generator.external-read`` --- GET …/offer-generators/{generatorId} (Assistenten-JSON mit aufgelösten Katalogblöcken nur für veröffentlichte Daten).
  • ``offer-generator.submit`` --- POST …/offer-generators/{generatorId}/submissions (Antworten validieren, optionale KI, Angebot speichern, Mail senden).
  • Persistenz: OfferGenerator, OfferGeneratorMailSettings, GeneratedOffer, OfferDelivery (Prisma). Nichts wird in messages oder Integrations-outboundMail geschrieben.

Assistenten-Konfiguration (Version 1)

Gespeichert als JSON auf ``OfferGenerator.wizardConfig``. Der letzte Schritt muss ein ``final``-Block sein.

Schrittform

  • id --- stabiler String-Bezeichner (referenziert durch ai-Block-includeStepIds).
  • visibleWhen (optional) --- entweder eine einzelne atomare Bedingung oder ein OR von AND-Gruppen:
  • Atomar --- Objekt { "var": "<name>", "equals": "<value>" } oder { "var": "<name>", "oneOf": ["…"] } (genau eines von equals oder oneOf).
  • Zusammengesetzt --- { "or": [ { "and": [ <atom>, … ] }, … ] }. Der Schritt ist sichtbar wenn irgendeine and-Gruppe übereinstimmt; innerhalb einer Gruppe muss jedes Atom übereinstimmen (gleiche Atomregeln wie oben). Verwenden Sie dies wenn mehrere unabhängige Kombinationen denselben Schritt anzeigen sollen.
  • block --- diskriminiert durch type:
  • ``text`` --- Optionales bodyMarkdown; fields[] mit name, optionalem label, kind (text | textarea | email | phone | boolean | pinGroup), required. Für pinGroup ist options[] (mindestens zwei value-/label-Paare) erforderlich; ein Wert wird im Feld-name-Variable gespeichert. Jedes Feld kann useMarkdownBody setzen (Boolean, Standard false): bei true ist markdownBody (Markdown) die Feldaufforderung statt label; vorbereitete Assistenten enthalten markdownBodyHtml pro Feld wenn markdownBody gesetzt ist. Felder können auch Katalog-Produktlisten anhängen (siehe offer-generator-field-product-lists{.interpreted-text role="ref"}).
  • ``choice`` --- variable; options[] mit value, label, optionaler setVariables-Map (Client kann in übermittelte Variablen einfügen).
  • ``product`` --- productIds[]; showFields; optionales showPrice (Standard true). Externes GET bettet resolvedProducts ein. Jede Zeile enthält ``images``, ``previewMediaId`` und ``previewImageUrl`` wenn das Katalogprodukt diese definiert (gleicher zusammengeführter-Locale-Vertrag wie die Market-externe Produkt-API: kein locales, optionale locale-Abfrage auf GET …/offer-generators/{id}). Für jeden Schlüssel in showFields, der ein Katalog-``MD_TEXT``-Attribut am aufgelösten Produkt ist, fügt der vorbereitete Assistent auch "<key>Html" (HTML aus Markdown, mit normalisierten Mailoo-Bild-URLs) neben dem rohen Markdown-/String-Wert hinzu.
  • ``priceList`` --- priceListId; optionaler lineSkuFilter; showFields; optionales showPrice. Externes GET bettet resolvedLines ein.
  • ``ai`` --- promptTemplate (unterstützt {{varName}}-Platzhalter); optionaler outputVariable (Standard aiText); optionale includeStepIds für zusätzlichen JSON-Kontext. Erfordert KI-Einstellungen am Generator (siehe unten).
  • ``final`` --- customerEmailVariable (muss mit einem übermittelten Variablennamen übereinstimmen); optionales submitLabel, bodyMarkdown und fields[]. Final-Felder verwenden dasselbe Schema und dieselben Validierungsregeln wie text.fields[] und werden im gleichen variables-Objekt übermittelt.

KI-Einstellungen (optional)

Gespeichert auf ``OfferGenerator.aiSettings`` als openai-compatible-JSON: baseUrl, model und verschlüsseltes apiKeyEncrypted (gleicher Verschlüsselungsmechanismus wie SMTP-Secrets). Die API gibt den Schlüssel nie zurück; Eigentümer-GET-Antworten zeigen nur hasApiKey.

Mail-Einstellungen (erforderlich für erfolgreiche E-Mail-Zustellung)

PUT /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}/mail-settings --- host, port, secure, user, from, optionales replyTo, optionale notificationEmail (BCC-artige interne Benachrichtigung), optionales smtpPassword (write-only bei PUT).

Wenn Mail-Einstellungen fehlen oder der SMTP-Versand fehlschlägt, erstellt die Übermittlung trotzdem ein ``GeneratedOffer`` mit Status ``FAILED`` und Diagnose; ``OfferDelivery``-Zeilen zeichnen Versuche pro Kanal auf (``CUSTOMER`` / ``NOTIFICATION``).

Eigentümer-API (Bearer)

  • GET /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators
  • POST /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators
  • GET /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}
  • PUT /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}
  • DELETE /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}
  • GET /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}/mail-settings --- gibt { data: null } zurück wenn noch keine Zeile existiert; ansonsten gleiche Nicht-Secret-Felder wie nach PUT.
  • PUT /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}/mail-settings

POST-Erstellungs-Body enthält key (Slug pro Integration), optionaler name, status, wizardConfig, optionale aiSettings (apiKey ist write-only wenn vorhanden).

Externe API (X-API-Key)

Ersetzen Sie Platzhalter mit Ihrer Projekt-UID, MARKET-Integrations-ID und Generator-ID (CUID).

  • GET {baseUrl}/api/v1/market/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}

    Gibt { data: { id, key, name, status, wizard } } zurück, wobei wizard.steps[].block bodyHtml, Feld-markdownBodyHtml, resolvedProducts und resolvedLines für vorbereitetes Rendering enthalten kann. Optionale Abfrage ``locale`` wählt die Katalogsprache für eingebettete Produkte (gleiche Regeln wie externes Market-GET …/products; Standard aus Integrations-defaultCatalogLocale, sonst en).

  • POST {baseUrl}/api/v1/market/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}/submissions

    Body: { "variables": { … } } --- alle Antworten nach Feld-/Choice-Variablennamen geordnet. Antwort: { data: { id, status, customerEmail } } mit status COMPLETED oder FAILED.

BFF- / Pre-Form-Muster

Halten Sie zwei beschränkte Schlüssel auf dem Server (niemals im Browser):

  1. Lese-Schlüssel --- offer-generator.external-read --- Server lädt den Assistenten einmalig (oder cached) und rendert die UI.
  2. Submit-Schlüssel --- offer-generator.submit --- Server sendet das finale variables-JSON nach Ihrer eigenen Validierung.

Katalogabfragen vom Client laufen weiterhin über Ihr BFF mit ``market.external-read``, wenn Sie Live-Produktdaten außerhalb des eingebetteten resolvedProducts-Snapshots benötigen.

Feld-Produktlisten {#offer-generator-field-product-lists}

Textartige Felder (text.fields[] und final.fields[]) können Produktempfehlungen neben der Eingabe anzeigen. Dies ermöglicht einem Integrator, ein reichhaltiges Pre-Form zu erstellen, ohne für jedes Feld eigene Katalogabfragen zu implementieren.

Für Nicht-Boolean-Felder hängen Sie products an:

{
  "name": "useCase",
  "label": "What do you need?",
  "kind": "textarea",
  "required": true,
  "products": {
    "productIds": ["product_cuid_1", "product_cuid_2"],
    "showFields": ["name", "sku"],
    "showPrice": true,
    "priceKindId": "price_kind_cuid"
  }
}

Für Boolean-Felder verwenden Sie separate Listen für jeden Zustand:

{
  "name": "installation",
  "label": "Include installation?",
  "kind": "boolean",
  "required": false,
  "productsWhenTrue": {
    "productIds": ["installation_service_id"],
    "showFields": ["name", "sku"],
    "showPrice": true,
    "priceKindId": "price_kind_cuid"
  }
}

Beim externen GET bereitet Mailoo diese Listen mit resolvedProducts für veröffentlichte Produkte vor. Rendern Sie aus resolvedProducts wenn vorhanden, und reagieren Sie robust wenn das Array leer ist (nicht veröffentlicht, gelöscht, gefiltert oder kein aktueller Preis).

Integrations-UI-Muster

Mailoos Dashboard-Vorschau kommt bewusst dem nahe, was eine externe Website implementieren kann. Empfohlene Client-Struktur:

  • Laden Sie den vorbereiteten Assistenten von Ihrem Server/BFF mit offer-generator.external-read und übergeben Sie nur das Assistenten-JSON an den Browser.
  • Pflegen Sie ein einzelnes variables-Objekt im Browser-State. Jedes Feld, jede Auswahl, jede Pin-Gruppe und jede KI-Vorschauausgabe schreibt per Variablenname in dieses Objekt.
  • Berechnen Sie sichtbare Schritte aus visibleWhen neu, wenn sich variables ändert.
  • Rendern Sie vorbereitetes bodyHtml / markdownBodyHtml wenn vorhanden; andernfalls rendern Sie den einfachen Markdown-Text als Fallback.
  • Rendern Sie resolvedProducts / resolvedLines aus dem vorbereiteten Assistenten, nicht indem Sie API-Schlüssel im Browser offenlegen.
  • Beim Absenden: validieren Sie sichtbare Schritte lokal für die UX, dann senden Sie { "variables": variables } von Ihrem Server/BFF mit dem offer-generator.submit-Schlüssel.

Minimaler Sichtbarkeitshelfer:

type Vars = Record<string, string | number | boolean | null>

function asString(value: unknown): string | undefined {
  if (value === null || value === undefined) return undefined
  if (typeof value === 'string') return value
  if (typeof value === 'number' || typeof value === 'boolean') {
    return String(value)
  }
  return undefined
}

function atomMatches(
  atom: { var: string; equals?: string; oneOf?: string[] },
  vars: Vars
): boolean {
  const current = asString(vars[atom.var])
  if (atom.equals !== undefined) {
    return current === asString(atom.equals)
  }
  if (Array.isArray(atom.oneOf)) {
    return current !== undefined && atom.oneOf.includes(current)
  }
  return false
}

function isVisible(step: { visibleWhen?: any }, vars: Vars): boolean {
  const when = step.visibleWhen
  if (!when) return true
  if (Array.isArray(when.or) && when.or.length > 0) {
    return when.or.some(
      (group: { and?: unknown }) =>
        Array.isArray(group.and) &&
        group.and.length > 0 &&
        group.and.every((atom: any) => atomMatches(atom, vars))
    )
  }
  return atomMatches(when, vars)
}

const visibleSteps = wizard.steps.filter((step) => isVisible(step, vars))

Choice-Blöcke sollten sowohl ihre Haupt-variable als auch optionale setVariables in einer State-Änderung aktualisieren:

function chooseOption(block: any, option: any) {
  setVars((prev) => ({
    ...prev,
    [block.variable]: option.value,
    ...(option.setVariables ?? {}),
  }))
}

Text- und Final-Felder teilen Rendering-Regeln:

function renderField(field: any) {
  const name = field.name
  const value = String(vars[name] ?? '')

  if (field.kind === 'boolean') {
    return (
      <input
        type="checkbox"
        checked={value.toLowerCase() === 'true'}
        onChange={(e) => setVar(name, e.target.checked ? 'true' : 'false')}
      />
    )
  }

  if (field.kind === 'pinGroup') {
    return field.options.map((option: any) => (
      <button type="button" onClick={() => setVar(name, option.value)}>
        {option.label}
      </button>
    ))
  }

  return (
    <input
      type={field.kind === 'email' ? 'email' : field.kind === 'phone' ? 'tel' : 'text'}
      value={value}
      onChange={(e) => setVar(name, e.target.value)}
    />
  )
}

Für Produktempfehlungen an Feldern verwenden Sie die vorbereitete Liste:

function fieldProductsFor(field: any) {
  if (field.kind === 'boolean') {
    const checked = String(vars[field.name] ?? '').toLowerCase() === 'true'
    return checked
      ? field.productsWhenTrue?.resolvedProducts ?? []
      : field.productsWhenFalse?.resolvedProducts ?? []
  }
  return field.products?.resolvedProducts ?? []
}

Submit-Muster:

async function submitOffer() {
  // Browser sendet an Ihre eigene Serverroute. Die Serverroute fügt X-API-Key hinzu.
  const res = await fetch('/api/offer-generator/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ variables: vars }),
  })
  if (!res.ok) throw new Error('Offer submission failed')
  return res.json()
}

Sicherheits- und Rendering-Hinweise

  • offer-generator.external-read- oder offer-generator.submit-Schlüssel niemals dem Browser offenlegen.
  • Vorbereitetes bodyHtml und markdownBodyHtml als HTML behandeln, das von Mailoos Server-Pipeline für den Assistenten generiert wurde, den Sie besitzen. Übergeben Sie keine beliebigen Besuchereingaben über dangerouslySetInnerHTML.
  • Lokale Browser-Validierung nur als Komfortfunktion betrachten. Die API validiert übermittelte Variablen erneut, einschließlich sichtbarer text- und final-Felder, Choice-Werte, Telefon-/E-Mail-Formate und customerEmailVariable.
  • Wenn die UI es Besuchern erlaubt, zurückzugehen und frühere Antworten zu ändern, entfernen oder berechnen Sie Variablen aus späteren ausgeblendeten Schritten vor dem Absenden neu, oder verlassen Sie sich auf die serverseitige Sichtbarkeitsschritt-Validierung zur Ablehnung inkonsistenter Nutzlasten.

Verwandt

  • market-catalog-external-api{.interpreted-text role="doc"} --- Produkt- und Preislistenlese.
  • market-catalog-csv{.interpreted-text role="doc"} --- Massen-Katalogbearbeitungen im Dashboard.
  • outbound-smtp{.interpreted-text role="doc"} --- wird nicht für Angebotsgenerator-Mail verwendet (Angebotsgenerator nutzt eigene SMTP-Tabelle).