> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fynn.eu/llms.txt
> Use this file to discover all available pages before exploring further.

# Vertriebskanäle über die API

> Wie Fynn den aktiven Vertriebskanal pro Request auflöst, wie du Kunden einem Kanal zuordnest, und die Endpunkte zum Verwalten von Kanälen, eigenen Domains und Branding.

Ein **Vertriebskanal** (Sales Channel) bildet eine Marke deiner Organisation ab und bestimmt Branding, Domain, erlaubte Zahlungsmethoden, Coupon-Verhalten und Kundenbereich. Diese Seite erklärt das Verhalten aus Entwicklersicht. Die fachliche Einführung findest du unter [Vertriebskanäle](/guide/tenant/sales-channels).

<Info>
  Jede Organisation hat **immer** einen Kanal mit dem technischen Namen `default`. Er ist der universelle Fallback und kann nicht gelöscht werden. Vertriebskanäle sind organisatorisch, sie isolieren **keine** Daten. Kunden, Subscriptions, Rechnungen, Produkte und Preise bleiben organisationsweit.
</Info>

<Note>
  In den Beispielen ist `$BASE_URL` die Basis-URL deiner Fynn-API und `$API_TOKEN` ein API-Token, das als `Authorization: Bearer` mitgeschickt wird.
</Note>

## Kanal-Auflösung

Fynn löst den aktiven Vertriebskanal **pro eingehendem Request** auf, nachdem der Tenant feststeht. Es gewinnt der erste Treffer in dieser Kette:

<Steps>
  <Step title="Header X-Sales-Channel-Id">
    Expliziter Kanal per Header `X-Sales-Channel-Id: <uuid>`. Wird für interne Wallet- und Tool-Zugriffe genutzt, wenn der Aufrufer den Kanal kennt.
  </Step>

  <Step title="Header X-Cart-Token">
    `X-Cart-Token: <token>` löst den Warenkorb auf; dessen Kanal stammt aus dem CheckoutLink. Das ist der reguläre Weg im Checkout der Kundenfront.
  </Step>

  <Step title="Domain (Host / Origin / Referer)">
    Der Host der Anfrage wird über die Customerfront-Domain zum Kanal aufgelöst. Bei Cross-Origin-Aufrufen der Kundenfront-SPA (der `Host` ist dann die API-Domain) wird zusätzlich `Origin`, dann `Referer`, dann `X-Source-Domain` herangezogen, dieselbe Header-Reihenfolge, mit der auch der Tenant aufgelöst wird.
  </Step>

  <Step title="Default-Kanal">
    Greift keiner der Schritte, wird der `default`-Kanal der Organisation verwendet.
  </Step>
</Steps>

<Warning>
  **Cross-Tenant-Guard:** Jeder aufgelöste Kanal wird gegen den Tenant des Requests geprüft. Gehört ein per Header oder Cart-Token referenzierter Kanal zu einer anderen Organisation, wird er still verworfen, und die Kette läuft zum nächsten Schritt weiter. Auf einen fremden Kanal fällt sie nie zurück.
</Warning>

Praktische Konsequenz: Verbindest du eine eigene Domain mit einem Kanal (siehe [Eigene Domains](#eigene-domains)), erreichen Aufrufe über diese Domain automatisch den richtigen Kanal, ohne dass du einen Header setzen musst. Eine Domain ist immer **genau einem** Kanal zugeordnet.

## Der `salesChannel` am Kunden

Jeder Kunde gehört genau einem Vertriebskanal an. Die Zuordnung steuert, welches Branding der Kunde in Bestätigungen, Dokumenten und im Self-Service sieht.

### Beim Anlegen und Aktualisieren (Eingabe)

Die Felder `POST /customers` und `PATCH /customers/{id}` akzeptieren ein optionales Feld `salesChannel`:

* Wert ist die **UUID** oder der **technische Name** des Kanals (zum Beispiel `"default"`).
* Der Kanal muss existieren und zur aktuellen Organisation gehören, andernfalls antwortet die API mit `422` und einer Validierungsmeldung am Feld.
* Lässt du das Feld bei der **Anlage** leer, weist Fynn den aktuell aufgelösten bzw. den `default`-Kanal zu.

```bash Kunde einem Kanal zuordnen theme={null}
curl -X POST "$BASE_URL/customers" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "company",
    "companyName": "Acme GmbH",
    "salesChannel": "b2b-shop"
  }'
```

```bash Kanal eines Kunden ändern theme={null}
curl -X PATCH "$BASE_URL/customers/{id}" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "salesChannel": "default" }'
```

### In der Ausgabe (Lesen)

In Kunden-Antworten (sowie eingebettet in CheckoutLink, Cart und Webhook-Envelopes) erscheint der Kanal als kompakte Referenz:

```json theme={null}
{
  "id": "0f9b…",
  "companyName": "Acme GmbH",
  "salesChannel": {
    "id": "45d60990-cde6-4139-b64f-79b79c6c6d98",
    "name": "b2b-shop",
    "brandName": "Acme B2B"
  }
}
```

| Feld        | Bedeutung                                                                                       |
| ----------- | ----------------------------------------------------------------------------------------------- |
| `id`        | UUID des Kanals.                                                                                |
| `name`      | Stabiler technischer Name (zum Beispiel `default`). Eindeutig pro Organisation, nicht änderbar. |
| `brandName` | Anzeigename der Marke für E-Mails, PDFs und Checkout.                                           |

## Vertriebskanäle verwalten

Die folgenden Endpunkte verwalten Kanäle und ihre Einstellungen. Sie erfordern ein API-Token mit der Berechtigung `sales-channel:read` (lesend) bzw. `sales-channel:write` (schreibend).

| Methode         | Pfad                                            | Zweck                                                             | Berechtigung                   |
| --------------- | ----------------------------------------------- | ----------------------------------------------------------------- | ------------------------------ |
| `GET`           | `/api/sales-channels`                           | Alle Kanäle der Organisation auflisten                            | `sales-channel:read`           |
| `POST`          | `/api/sales-channels`                           | Kanal anlegen                                                     | `sales-channel:write`          |
| `GET`           | `/api/sales-channels/{id}`                      | Einen Kanal abrufen                                               | `sales-channel:read`           |
| `PATCH`         | `/api/sales-channels/{id}`                      | Stammdaten, Coupons, Zahlungsmethoden, Weiterleitungs-URLs ändern | `sales-channel:write`          |
| `DELETE`        | `/api/sales-channels/{id}`                      | Kanal löschen (nur ohne Referenzen, nicht den `default`)          | `sales-channel:write`          |
| `PATCH`         | `/api/sales-channels/{id}/appearance`           | Logo, Farben, Rechtstexte ändern                                  | `sales-channel:write`          |
| `GET` · `PATCH` | `/api/sales-channels/{id}/customer-area`        | Kundenbereich lesen/ändern                                        | `sales-channel:read` · `write` |
| `GET` · `PATCH` | `/api/sales-channels/{id}/email-settings`       | Absender, Sendedomain, Mail-Vorlage                               | `sales-channel:read` · `write` |
| `GET` · `PATCH` | `/api/sales-channels/{id}/document-settings`    | Belegvorlagen (Logo-Position, Fußbereich)                         | `sales-channel:read` · `write` |
| `GET`           | `/api/sales-channels/{id}/domain`               | Standard-Domain des Kanals                                        | `sales-channel:read`           |
| `GET`           | `/api/sales-channels/payment-methods/available` | Verfügbare Zahlungsmethoden (aktive Gateways)                     | `sales-channel:read`           |

### Kanal anlegen

```bash theme={null}
curl -X POST "$BASE_URL/api/sales-channels" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "b2b-shop",
    "brandName": "Acme B2B",
    "websiteUrl": "https://b2b.acme.example"
  }'
```

| Feld         | Pflicht | Regeln                                                                                                                        |
| ------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `name`       | ja      | Technischer Name. Nur Kleinbuchstaben, Ziffern und Bindestriche (`^[a-z0-9-]+$`), 1 bis 255 Zeichen. **Nicht** mehr änderbar. |
| `brandName`  | ja      | Anzeigename der Marke, 1 bis 255 Zeichen.                                                                                     |
| `websiteUrl` | nein    | Muss `https` sein.                                                                                                            |

Beim Anlegen wird automatisch eine Standard-Customerfront-Domain aus `name` und einem festen Suffix erzeugt. Der `name` ist später unveränderlich, weil er in URLs und Logs auftaucht.

### Kanal aktualisieren

`PATCH /api/sales-channels/{id}` ist partiell, nur übergebene Felder werden geändert.

```bash theme={null}
curl -X PATCH "$BASE_URL/api/sales-channels/{id}" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "allowCoupons": false,
    "allowedPaymentMethods": ["sepa_direct_debit"],
    "paymentSuccessUrl": "https://b2b.acme.example/danke"
  }'
```

| Feld                                                                                       | Typ            | Bedeutung                                                                                                           |
| ------------------------------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------- |
| `brandName`                                                                                | string         | Anzeigename der Marke.                                                                                              |
| `websiteUrl`                                                                               | string (https) | „Zurück zur Website"-Link.                                                                                          |
| `phone`                                                                                    | string         | Kontakt-Telefonnummer.                                                                                              |
| `allowCoupons`                                                                             | boolean        | Kanal-Gate für Coupons (siehe unten).                                                                               |
| `allowedPaymentMethods`                                                                    | string\[]      | Whitelist der Zahlungsmethoden. Leer = keine Einschränkung. Jeder Wert muss auf ein aktives Gateway abbildbar sein. |
| `paymentSuccessUrl` `paymentCancelUrl` `paymentFailureUrl` `paymentPendingUrl` `returnUrl` | string (https) | Weiterleitungs-URLs nach dem Zahlvorgang.                                                                           |

### Vollständige Kanal-Repräsentation

`GET /api/sales-channels/{id}` liefert die volle Sicht inklusive eingebetteter Appearance-Felder:

```json theme={null}
{
  "id": "45d60990-cde6-4139-b64f-79b79c6c6d98",
  "name": "default",
  "isDefault": true,
  "brandName": "myLife AG",
  "websiteUrl": "https://mylife.de",
  "phone": "+49 30 1234567",
  "allowCoupons": true,
  "allowedPaymentMethods": [],
  "paymentSuccessUrl": null,
  "paymentCancelUrl": null,
  "paymentFailureUrl": null,
  "paymentPendingUrl": null,
  "returnUrl": null,
  "logoUrl": "https://…/logo.svg",
  "primaryColor": "#000000",
  "secondaryColor": "#FF0000",
  "privacyUrl": "https://mylife.de/datenschutz",
  "conditionsUrl": "https://mylife.de/agb",
  "createdAt": "2026-01-10T08:00:00+00:00",
  "updatedAt": "2026-06-17T12:00:00+00:00"
}
```

## Coupon-Gate

`allowCoupons` ist ein **Kanal-weites Gate**. Steht es auf `false`, sind Coupons in keinem CheckoutLink dieses Kanals einlösbar, egal was der einzelne CheckoutLink erlaubt. Ein CheckoutLink kann Coupons nur weiter **einschränken**, nie gegen den Kanal **erlauben**.

| Kanal `allowCoupons` | CheckoutLink | Einlösbar            |
| -------------------- | ------------ | -------------------- |
| `true`               | erlaubt      | ja                   |
| `true`               | verboten     | nein                 |
| `false`              | erlaubt      | nein (Kanal gewinnt) |
| `false`              | verboten     | nein                 |

Neu angelegte Kanäle haben `allowCoupons = true`.

## Zahlungsmethoden-Whitelist

`allowedPaymentMethods` wirkt als Whitelist:

* **Leeres Array** → keine kanalspezifische Einschränkung; alle Methoden mit aktivem Gateway sind erlaubt.
* **Befülltes Array** → nur die gelisteten Methoden, sofern ihr Gateway aktiv konfiguriert ist.

Beim Speichern validiert Fynn, dass jeder Wert auf ein für die Organisation aktiv konfiguriertes Payment-Gateway abbildbar ist. Die im Kontext verfügbaren Methoden liefert `GET /api/sales-channels/payment-methods/available`.

## Eigene Domains

Jeder Kanal hat genau eine **Standard-Domain** (automatisch, nicht editierbar) und optional **eine eigene Domain**. Ein einzelner CNAME genügt, das TLS-Zertifikat wird automatisch ausgestellt und erneuert. Es gibt **keinen** selbstverwalteten TXT-Nachweis.

| Methode  | Pfad                                            | Zweck                                     |
| -------- | ----------------------------------------------- | ----------------------------------------- |
| `GET`    | `/api/sales-channels/{id}/custom-domain`        | Domain + DNS-Anweisungen + Status abrufen |
| `POST`   | `/api/sales-channels/{id}/custom-domain`        | Eigene Domain hinzufügen                  |
| `POST`   | `/api/sales-channels/{id}/custom-domain/verify` | Status sofort prüfen („Verifizieren")     |
| `DELETE` | `/api/sales-channels/{id}/custom-domain`        | Eigene Domain entfernen                   |

### Domain hinzufügen

```bash theme={null}
curl -X POST "$BASE_URL/api/sales-channels/{id}/custom-domain" \
  -H "Authorization: Bearer $API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "host": "shop.example.com" }'
```

Die Antwort enthält die DNS-Anweisungen und beide Status:

```json theme={null}
{
  "id": "…",
  "host": "shop.example.com",
  "status": "pending",
  "sslStatus": "pending_validation",
  "isDefault": false,
  "dnsInstructions": [
    { "type": "CNAME", "name": "shop.example.com", "value": "cname.customerfront.app" }
  ],
  "note": "Lege den CNAME-Eintrag bei deinem DNS-Anbieter an. Das TLS-Zertifikat wird automatisch ausgestellt, sobald der CNAME auflöst …"
}
```

`dnsInstructions` enthält immer den CNAME und zusätzlich etwaige Validierungs-Einträge, die du ebenfalls anlegen musst.

### Die zwei Status

Eine eigene Domain hat zwei voneinander unabhängige Status:

| Feld        | Bedeutung                     | Werte                                              |
| ----------- | ----------------------------- | -------------------------------------------------- |
| `status`    | DNS-Kontrolle / Verifizierung | `pending`, `verified`                              |
| `sslStatus` | TLS-Lebenszyklus              | `pending`, `pending_validation`, `active`, `error` |

Die Domain wird erst **produktiv** (und zur Standard-Adresse des Kanals), wenn `sslStatus = active` ist. Ab da löst Fynn Anfragen über diese Domain auf den Kanal auf.

### Verifizieren

`POST /api/sales-channels/{id}/custom-domain/verify` prüft den Status einmal synchron:

* Ist der Status `active`, wird die Domain verifiziert und zur Standard-Customerfront-Domain des Kanals befördert.
* `pending` / `pending_validation` werden ohne Fehler zurückgegeben, versuche es später erneut.
* `error` (das Zertifikat kann nicht ausgestellt werden) führt zu einer Fehlerantwort.

<Tip>
  Du musst nicht aktiv pollen: Ein wiederkehrender Reconcile-Job prüft den SSL-Status im Hintergrund und schaltet die Domain frei, sobald das Zertifikat steht. Der Verify-Endpunkt ist nur der „jetzt prüfen"-Pfad und idempotent, eine bereits aktive Domain bleibt unverändert.
</Tip>

### Entfernen

`DELETE /api/sales-channels/{id}/custom-domain` entfernt die eigene Domain und setzt die Kanal-Adresse auf die Standard-Domain zurück. Der Checkout bleibt also durchgehend erreichbar.

## Weiterleitungs-URLs und `returnUrl`

Die optionalen URLs auf dem Kanal steuern, wohin der Kunde nach dem Zahlvorgang geleitet wird. Bleibt eine URL leer, fällt der Kunde auf die Standardseite der Kundenfront (`/self-service/transaction/{status}`) zurück.

| Feld                | Auslöser                                                                      |
| ------------------- | ----------------------------------------------------------------------------- |
| `paymentSuccessUrl` | Status `booked` / `captured` (erfolgreich).                                   |
| `paymentCancelUrl`  | Kunde bricht beim Anbieter ab.                                                |
| `paymentFailureUrl` | Explizite Ablehnung durch den Anbieter.                                       |
| `paymentPendingUrl` | Status `pending` / `authorized` (zum Beispiel wartende SEPA-Lastschrift).     |
| `returnUrl`         | Nach einem API-getriebenen Payment-Method-Setup (`PaymentMethodSource::Api`). |

Startest du Payment-Method-Setups über die API, wird nach Abschluss die Kanal-`returnUrl` aufgerufen. Lässt du sie leer, kannst du pro Request dynamisch ein `redirectUrl` im PaymentMethodSetup mitgeben, das stattdessen verwendet wird.

## Webhooks

Jeder ausgehende Webhook trägt den Header `X-Sales-Channel` mit dem **technischen Namen** des Kanals, dem das auslösende Objekt zugeordnet ist (oder `default`, wenn keiner gesetzt ist). So routest du eingehende Events empfängerseitig nach Marke, ohne den Payload zu parsen.

```http theme={null}
POST /your-webhook-endpoint HTTP/1.1
X-Sales-Channel: b2b-shop
Content-Type: application/json
```

## Verwandte Themen

<CardGroup cols={2}>
  <Card title="Vertriebskanäle (Anleitung)" icon="tag" href="/guide/tenant/sales-channels">
    Die Einstellungen in der Wallet, Schritt für Schritt mit Screenshots.
  </Card>

  <Card title="Kunden" icon="users" href="/guide/customers/introduction">
    Kundenverwaltung und das `salesChannel`-Feld im Kontext.
  </Card>

  <Card title="Eigene E-Mail-Domain" icon="envelope" href="/guide/notifications/custom-email-domain">
    Sendedomain pro Kanal einrichten.
  </Card>

  <Card title="Webhooks" icon="bell" href="/guide/webhooks/introduction">
    Echtzeit-Events empfangen und nach Kanal routen.
  </Card>
</CardGroup>
