# Formular für Beratungsanfragen für ein Dienstleistungsunternehmen bauen

> Ein Formular für Beratungsanfragen für Steuerberater, Anwälte, Coaches und Praxen, das fragt, wie und wann angerufen werden soll, und heikle Details für das Gespräch lässt.

Veröffentlicht am 2026-10-25 · https://ikiform.com/de/guides/lead-capture/consultation-request

Frag nach Name, E-Mail, wie und ungefähr wann die Person erreichbar ist und nach einem groben Thema, und heb jedes Detail ihrer Situation für das Gespräch auf. Erfasse ihre Zeitzone aus dem Browser, damit ein gewünschter Vormittag ihren Vormittag meint. Setze Regeln, die das Formularschema nicht ausdrücken kann, etwa eine Telefonnummer nur bei gewähltem Telefonanruf, in deinem eigenen Code durch.

## Was an einer Beratungsanfrage anders ist

Eine Beratungsanfrage kommt von einem Menschen mit einem Problem: ein Brief vom Finanzamt, ein Mietstreit, ein Unternehmen, das gegründet werden soll. Er will es erklären, und das Formular lädt ihn dazu ein. Genau das ist die Falle. Was er eintippt, wird mit der Einsendung gespeichert, in Benachrichtigungs-E-Mails angezeigt und per Webhook dorthin gesendet, wohin du ihn richtest. Kontonummern und Falldetails gehören an keinen dieser Orte.

Die Aufgabe des Formulars ist also enger, als sie aussieht: wer die Person ist, wie und wann man sie erreicht und ungefähr worum es geht. Alles andere wartet auf das Gespräch, wo du bestimmst, was aufgeschrieben und wo es aufbewahrt wird.

Diagramm: Was ein Beratungsformular fragt und was auf das Gespräch wartet

1. Das Formular fragt, wer die Person ist: Name und E-Mail.
2. Es fragt, wie man sie erreicht: E-Mail, Telefon oder Video.
3. Es fragt, wann: ein bevorzugter Tag, ein Zeitfenster und ihre Zeitzone.
4. Es fragt, worum es geht, als ein grobes Thema.
5. Das Gespräch behandelt ihre Situation in ihren eigenen Worten.
6. Dokumente laufen über einen Kanal, den du wählst, nicht über das Formular.
7. Kontonummern, Ausweisnummern und Unterlagen werden nie ins Formular getippt.
8. Das Gespräch endet mit deiner Antwort zu Eignung und Honorar.

Die obere Reihe reicht, um das Gespräch zu buchen und vorzubereiten. Die untere Reihe ist das, wofür das Gespräch da ist.

## Die Felder

Formular: Free consultation

- **Name** (`name`, text, Pflichtfeld)
- **E-Mail** (`email`, email, Pflichtfeld)
- **Wie sollen wir dich erreichen?** (`contact_method`, radio, Pflichtfeld)
- **Telefon** (`phone`, phone)
- **Worum geht es?** (`topic`, select, Pflichtfeld)
- **Bevorzugter Tag** (`preferred_date`, date)
- **Beste Uhrzeit** (`time_window`, radio)
- **Zeitzone** (`timezone`, text)
- **Sollten wir vorab etwas wissen?** (`note`, textarea)

Wähle **Phone call** und lass das Telefon leer: Das Schema akzeptiert es, weil ein Feld nicht nur manchmal Pflicht sein kann. Der Code unten schließt diese Lücke.

| Feld | Warum es so aufgebaut ist |
| --- | --- |
| `contact_method` | Ein `radio`, damit der erste Kontakt so stattfindet, wie gewünscht |
| `phone` | Im Schema optional, von deinem Code verlangt, wenn ein Telefonanruf gewählt wird |
| `preferred_date` | Ein `date`-Feld speichert `YYYY-MM-DD`. Sein `min` im Schema ist fest, also setzt die Seite im Browser morgen als `min` |
| `timezone` | Aus dem Browser befüllt mit `Intl.DateTimeFormat().resolvedOptions().timeZone`, etwa `Europe/Berlin` |
| `note` | Auf 500 Zeichen begrenzt. Benachrichtigungs-E-Mails zeigen bis zu 500 Zeichen pro Wert, also passt die ganze Notiz hinein |

## Bedingte Regeln in deinem eigenen Code

Ikiform validiert jedes Feld für sich. Regeln, die von einem anderen Feld abhängen, etwa Telefon nur bei Telefonanrufen, laufen in deinem Code. Server-Versionen können sie wirklich durchsetzen; Browser-Versionen führen den Besucher und senden nichts, was er nicht senden musste.

**HTML**

Das Skript setzt `required` auf den Telefon-Input, wenn **Phone call** gewählt wird, setzt morgen als frühesten Tag im eigenen Kalender des Besuchers und füllt die versteckte Zeitzone.

`consultation.html`:

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST" id="consult">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required />
  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <fieldset>
    <legend>How should we reach you?</legend>
    <label><input type="radio" name="contact_method" value="email" checked /> Email</label>
    <label><input type="radio" name="contact_method" value="phone" /> Phone call</label>
    <label><input type="radio" name="contact_method" value="video" /> Video call</label>
  </fieldset>
  <label for="phone">Phone</label>
  <input id="phone" name="phone" type="tel" autocomplete="tel" />

  <label for="topic">What is it about?</label>
  <select id="topic" name="topic" required>
    <option value="new_business">Starting a business</option>
    <option value="tax">Tax and bookkeeping</option>
    <option value="other">Something else</option>
  </select>

  <label for="preferred_date">Preferred day</label>
  <input id="preferred_date" name="preferred_date" type="date" />
  <fieldset>
    <legend>Best time</legend>
    <label><input type="radio" name="time_window" value="morning" /> Morning</label>
    <label><input type="radio" name="time_window" value="afternoon" /> Afternoon</label>
    <label><input type="radio" name="time_window" value="evening" /> Evening</label>
  </fieldset>
  <input type="hidden" name="timezone" />

  <label for="note">Anything we should know first?</label>
  <textarea id="note" name="note" rows="3" maxlength="500"></textarea>
  <p>A sentence or two. Please don't include account numbers or personal records.</p>

  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Request a consultation</button>
</form>

<script>
  const form = document.getElementById("consult");
  const phone = form.elements.phone;
  const tomorrow = new Date(Date.now() + 86400000);
  const local = new Date(tomorrow.getTime() - tomorrow.getTimezoneOffset() * 60000);

  form.elements.timezone.value = Intl.DateTimeFormat().resolvedOptions().timeZone;
  form.elements.preferred_date.min = local.toISOString().slice(0, 10);
  form.addEventListener("change", () => {
    phone.required = form.elements.contact_method.value === "phone";
  });
</script>
```

**Next.js**

Die Server Action lehnt einen Telefonanruf ohne Nummer vor dem Senden ab, löscht eine Nummer, die eingetippt und dann abgewählt wurde, und leitet auf eine Dankeseite weiter. Rendere das Formular mit `useActionState` und zeig `state.error` an.

`app/consultation/actions.ts`:

```ts
"use server";

import { headers } from "next/headers";
import { redirect } from "next/navigation";

const KEYS = ["name", "email", "contact_method", "phone", "topic", "preferred_date", "time_window", "timezone", "note", "_gotcha"];

export async function requestConsultation(_previous: Record<string, string>, formData: FormData) {
  const values = Object.fromEntries(KEYS.map((key) => [key, String(formData.get(key) ?? "").trim()]));

  if (values.contact_method === "phone" && !values.phone) {
    return { ...values, error: "Add a phone number, or choose email or video." };
  }
  if (values.contact_method !== "phone") values.phone = "";
  const origin = (await headers()).get("origin");

  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
      ...(origin ? { Origin: origin } : {}),
    },
    body: JSON.stringify(values),
  });
  const result = await response.json().catch(() => null);
  if (!result?.ok) {
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    const [field, messages] = Object.entries(fields)[0] ?? [];
    return { ...values, error: field ? `${field}: ${messages?.[0]}` : (result?.error?.message ?? "Couldn't send. Try again.") };
  }
  redirect("/consultation/thanks");
}
```

**Vue**

Den Telefon-Input gibt es nur, wenn ein Telefonanruf gewählt ist, und leere Werte werden vor dem Post entfernt, also kommen optionale Felder als fehlend statt leer an.

`src/components/ConsultationForm.vue`:

```vue
<script setup lang="ts">
import { computed, reactive, ref } from "vue";

const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

const request = reactive({
  name: "",
  email: "",
  contact_method: "email",
  phone: "",
  topic: "new_business",
  preferred_date: "",
  time_window: "",
  note: "",
  timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
});
const status = ref<"idle" | "sending" | "sent">("idle");
const error = ref("");
const wantsCall = computed(() => request.contact_method === "phone");

async function send() {
  status.value = "sending";
  error.value = "";
  const body = Object.fromEntries(Object.entries(request).filter(([, value]) => value !== ""));
  if (!wantsCall.value) delete body.phone;
  try {
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: { Accept: "application/json", "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    const result = await response.json();
    if (result.ok) {
      status.value = "sent";
      return;
    }
    const first = Object.entries(result.error.fields ?? {})[0] as [string, string[]] | undefined;
    error.value = first ? `${first[0]}: ${first[1][0]}` : result.error.message;
  } catch {
    error.value = "Couldn't reach us. Try again, or call the office.";
  }
  status.value = "idle";
}
</script>

<template>
  <p v-if="status === 'sent'" role="status">Thanks, {{ request.name }}. We'll contact you by {{ request.contact_method }} within one working day.</p>
  <form v-else @submit.prevent="send">
    <label for="name">Name</label>
    <input id="name" v-model="request.name" autocomplete="name" required />
    <label for="email">Email</label>
    <input id="email" v-model="request.email" type="email" autocomplete="email" required />
    <fieldset>
      <legend>How should we reach you?</legend>
      <label><input v-model="request.contact_method" type="radio" value="email" /> Email</label>
      <label><input v-model="request.contact_method" type="radio" value="phone" /> Phone call</label>
      <label><input v-model="request.contact_method" type="radio" value="video" /> Video call</label>
    </fieldset>
    <template v-if="wantsCall">
      <label for="phone">Phone</label>
      <input id="phone" v-model="request.phone" type="tel" autocomplete="tel" required />
    </template>
    <label for="topic">What is it about?</label>
    <select id="topic" v-model="request.topic">
      <option value="new_business">Starting a business</option>
      <option value="tax">Tax and bookkeeping</option>
      <option value="other">Something else</option>
    </select>
    <label for="preferred_date">Preferred day</label>
    <input id="preferred_date" v-model="request.preferred_date" type="date" />
    <label for="note">Anything we should know first?</label>
    <textarea id="note" v-model="request.note" rows="3" maxlength="500" />
    <p v-if="error" role="alert">{{ error }}</p>
    <button :disabled="status === 'sending'">{{ status === "sending" ? "Sending" : "Request a consultation" }}</button>
  </form>
</template>
```

Die serverseitigen Versionen senden mit einem Secret Key, also überspringt Ikiform **Allowed domains** und seine Menschlichkeitsprüfung. Jeder Post kommt dann von der IP deines Servers, und das Standard-Limit von 5 pro Minute und Besucher zählt alle deine Besucher zusammen. Wähle das [Rate-Limit](https://ikiform.com/de/docs/rate-limits.md) Relaxed oder Custom für ein Formular, das du über einen Server sendest, und halte `IKIFORM_SECRET_KEY` in Umgebungsvariablen, die nur der Server sieht. Die Serverversionen leiten den `Origin`-Header des Besuchers weiter, weil ein serverseitiger Fetch keinen sendet und Ikiform daran kostenlose `localhost`-Tests erkennt.

## Heikle Details aus dem Formular heraushalten

- Sag es im Hilfetext, wie das Formular oben. Leute folgen einer klaren Anweisung öfter, als man denkt.
- Füge keine Felder für Ausweisnummern, Kontonummern oder medizinische Details hinzu. Hat dein Berufsstand Regeln zu Mandantendaten, prüfe sie, bevor du mehr als Kontaktdaten erhebst, und erhebe den Rest über einen Kanal, der sie erfüllt.
- Auch Feldnamen zählen. Ikiform hält die Benachrichtigungs-E-Mail zur Prüfung zurück, wenn ein Feldname wie eine Zugangsdaten-Angabe aussieht, darunter Namen mit `password`, `ssn`, `cvv` oder `seed`. Ein Feld namens `seed_round` im Formular einer Startup-Beratung löst das aus. Nenne es `funding_stage`.
- Richte Webhooks nur dorthin, wo das ganze Team Mandantennamen lesen darf, etwa einen privaten Channel, keinen firmenweiten.

## Die erste Antwort

Die Benachrichtigungs-E-Mail kommt mit **Reply** auf den Besucher gerichtet an, sodass die erste Antwort direkt aus deinem Posteingang kommen kann. Gib ihr eine Aufgabe: einen Termin bestätigen. Zu zwei konkreten Terminen innerhalb des Zeitfensters sagt man leichter Ja als zu der Frage, wann es passt.

- Rechne das Zeitfenster in deine Zeit um, bevor du schreibst. Ein `morning` in `Australia/Sydney` ist in London der Vorabend.
- Biete zwei Termine in der Zeitzone der Person an und nenne die Zone in der E-Mail, damit niemand eine Stunde zu früh erscheint.
- Sag, wie du anrufst: unter der angegebenen Nummer oder per Video-Link, den du mit der Einladung schickst.
- Liegt das Thema außerhalb dessen, was du machst, sag das in der ersten Antwort und empfiehl jemanden, der es macht. Auch diese Antwort lohnt sich.

## Prüfen, ob es funktioniert

1. Wähle **Phone call**, lass das Telefon leer und sende ab. Der Browser oder dein Server stoppt es mit einer Meldung.
2. Wähle **Email**, sende ab und öffne die Einsendung. Sie hat keinen Wert für `phone` und enthält deine Zeitzone, etwa `America/Chicago`.
3. Versuch, heute als bevorzugten Tag zu wählen. Die Datumsauswahl bietet ihn nicht an.
4. Prüf die Benachrichtigungs-E-Mail. Die Notiz erscheint vollständig, und **Reply** schreibt an den Besucher.
5. Liste deine Feld-IDs auf und prüfe, dass keine `password`, `ssn`, `cvv` oder `seed` enthält.

## Stolperfallen

- **Datum ohne Zeitzone.** `preferred_date` ist ein Kalendertag, kein Zeitpunkt. Lies es zusammen mit `timezone` und `time_window`, bevor du anrufst.
- **Radios ohne Auswahl.** Eine Radio-Gruppe ohne Auswahl sendet nichts. Für `time_window`, das optional ist, ist das in Ordnung, und deshalb startet `contact_method` mit vorausgewähltem **Email**.
- **Keine Buchungsbestätigung.** Ikiform schreibt der Person, die das Formular ausfüllt, keine E-Mail. Deine erste Antwort bestätigt den Termin. Sollen Leute selbst einen Termin wählen, übergib an einen Terminplaner wie im [Formular für Demo-Anfragen](https://ikiform.com/de/guides/lead-capture/demo-request.md).
- **Telefonformat.** Das Telefonfeld akzeptiert 6 bis 15 Ziffern, wie eingetippt. Aktiviere `e164`, wenn du aus einem System wählst, das `+14155550123` braucht; Nummern ohne Ländervorwahl werden dann abgelehnt.

## Fragen

### Was sollte ein Formular für kostenlose Beratungen fragen?

Name, E-Mail, wie die Person kontaktiert werden will, einen bevorzugten Tag und eine Uhrzeit und ein grobes Thema. Die ganze Situation, Dokumente und alle Nummern bleiben für die Beratung selbst.

### Kann ich die Telefonnummer nur dann zur Pflicht machen, wenn jemand einen Telefonanruf wählt?

Nicht in der Felddefinition, die jedes Feld für sich validiert. Setze required im Browser auf den Input, wenn Telefon gewählt ist, und prüfe es in deinem Server-Code vor dem Senden.

### Woher weiß ich, in welcher Zeitzone die gewünschte Uhrzeit liegt?

Befülle im Browser ein verstecktes timezone-Feld aus Intl.DateTimeFormat().resolvedOptions().timeZone. Es speichert einen IANA-Namen wie Europe/London neben dem gewünschten Tag und Zeitfenster.

### Kann Ikiform der Person, die das Formular gesendet hat, eine E-Mail schicken?

Nein. Ikiform schickt E-Mails an dich und dein Team, nicht an den Absender, es gibt also keinen Autoresponder und keine Bestätigungs-E-Mail. Sende einen Webhook an dein E-Mail-Tool und verschicke die Bestätigung von dort.

## Weitere Anleitungen

- [Formular zur Lead-Erfassung](https://ikiform.com/de/guides/lead-capture.md): Ein Lead-Formular auf deiner eigenen Site, das dich per E-Mail mit Reply-To benachrichtigt, den Team-Chat anpingt und dein CRM füttert, damit du antwortest, solange der Lead noch da ist.
- [Formular für Demo-Anfragen](https://ikiform.com/de/guides/lead-capture/demo-request.md): Ein Formular zum Demo-Buchen, das den Lead speichert und den Besucher dann mit vorausgefülltem Namen und E-Mail zu Cal.com oder Calendly schickt.
- [Formular für Projektanfragen](https://ikiform.com/de/guides/lead-capture/project-inquiry.md): Ein Formular für neue Projekte für Studios und Freelancer, das Anfragen nach Budget und Zeitpunkt in heiß, warm und unpassend sortiert, bevor jemand das Briefing liest.
- [Formular für Angebotsanfragen](https://ikiform.com/de/guides/lead-capture/quote-request.md): Ein Formular für Angebotsanfragen, das Briefings als geteilte Links statt als Uploads sammelt, jeder Anfrage ein Fälligkeitsdatum gibt und linklastige Leads aus dem Spam hält.
- [Leads an HubSpot oder Pipedrive](https://ikiform.com/de/guides/lead-capture/leads-to-crm.md): Bilde jeden signierten Ikiform-Webhook auf HubSpots Kontakt-Upsert oder die Personen- und Lead-Aufrufe von Pipedrive ab, ohne doppelte Kontakte bei einer wiederholten Zustellung.
- [Lead-Formular mit Next.js](https://ikiform.com/de/guides/lead-capture/nextjs.md): Ein Lead-Formular für den Next.js App Router, das in einer Server Action mit Zod validiert, UTMs des ersten Kontakts in einem Cookie hält und qualifizierte Leads in deinen Kalender schickt.
