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 vonmarket.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 inmessagesoder Integrations-outboundMailgeschrieben.
Assistenten-Konfiguration (Version 1)
Gespeichert als JSON auf ``OfferGenerator.wizardConfig``. Der letzte Schritt muss ein ``final``-Block sein.
Schrittform
id--- stabiler String-Bezeichner (referenziert durchai-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 vonequalsoderoneOf). - Zusammengesetzt ---
{ "or": [ { "and": [ <atom>, … ] }, … ] }. Der Schritt ist sichtbar wenn irgendeineand-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 durchtype:- ``text`` --- Optionales
bodyMarkdown;fields[]mitname, optionalemlabel,kind(text|textarea|email|phone|boolean|pinGroup),required. FürpinGroupistoptions[](mindestens zweivalue-/label-Paare) erforderlich; ein Wert wird im Feld-name-Variable gespeichert. Jedes Feld kannuseMarkdownBodysetzen (Boolean, Standard false): bei true istmarkdownBody(Markdown) die Feldaufforderung stattlabel; vorbereitete Assistenten enthaltenmarkdownBodyHtmlpro Feld wennmarkdownBodygesetzt ist. Felder können auch Katalog-Produktlisten anhängen (sieheoffer-generator-field-product-lists{.interpreted-text role="ref"}). - ``choice`` ---
variable;options[]mitvalue,label, optionalersetVariables-Map (Client kann in übermittelte Variablen einfügen). - ``product`` ---
productIds[];showFields; optionalesshowPrice(Standard true). Externes GET bettetresolvedProductsein. Jede Zeile enthält ``images``, ``previewMediaId`` und ``previewImageUrl`` wenn das Katalogprodukt diese definiert (gleicher zusammengeführter-Locale-Vertrag wie die Market-externe Produkt-API: keinlocales, optionalelocale-Abfrage aufGET …/offer-generators/{id}). Für jeden Schlüssel inshowFields, 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; optionalerlineSkuFilter;showFields; optionalesshowPrice. Externes GET bettetresolvedLinesein. - ``ai`` ---
promptTemplate(unterstützt{{varName}}-Platzhalter); optionaleroutputVariable(StandardaiText); optionaleincludeStepIdsfür zusätzlichen JSON-Kontext. Erfordert KI-Einstellungen am Generator (siehe unten). - ``final`` ---
customerEmailVariable(muss mit einem übermittelten Variablennamen übereinstimmen); optionalessubmitLabel,bodyMarkdownundfields[]. Final-Felder verwenden dasselbe Schema und dieselben Validierungsregeln wietext.fields[]und werden im gleichenvariables-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-generatorsPOST /api/v1/projects/{projectUid}/integrations/{integrationId}/offer-generatorsGET /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 nachPUT.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, wobeiwizard.steps[].blockbodyHtml, Feld-markdownBodyHtml,resolvedProductsundresolvedLinesfü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, sonsten). -
POST {baseUrl}/api/v1/market/{projectUid}/integrations/{integrationId}/offer-generators/{generatorId}/submissionsBody:
{ "variables": { … } }--- alle Antworten nach Feld-/Choice-Variablennamen geordnet. Antwort:{ data: { id, status, customerEmail } }mitstatusCOMPLETEDoderFAILED.
BFF- / Pre-Form-Muster
Halten Sie zwei beschränkte Schlüssel auf dem Server (niemals im Browser):
- Lese-Schlüssel ---
offer-generator.external-read--- Server lädt den Assistenten einmalig (oder cached) und rendert die UI. - Submit-Schlüssel ---
offer-generator.submit--- Server sendet das finalevariables-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-readund ü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
visibleWhenneu, wenn sichvariablesändert. - Rendern Sie vorbereitetes
bodyHtml/markdownBodyHtmlwenn vorhanden; andernfalls rendern Sie den einfachen Markdown-Text als Fallback. - Rendern Sie
resolvedProducts/resolvedLinesaus 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 demoffer-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- oderoffer-generator.submit-Schlüssel niemals dem Browser offenlegen.- Vorbereitetes
bodyHtmlundmarkdownBodyHtmlals HTML behandeln, das von Mailoos Server-Pipeline für den Assistenten generiert wurde, den Sie besitzen. Übergeben Sie keine beliebigen Besuchereingaben überdangerouslySetInnerHTML. - Lokale Browser-Validierung nur als Komfortfunktion betrachten. Die API validiert übermittelte Variablen erneut, einschließlich sichtbarer
text- undfinal-Felder, Choice-Werte, Telefon-/E-Mail-Formate undcustomerEmailVariable. - 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).