# サービス業向けの相談申し込みフォームを作る

> 会計士、弁護士、コーチ、クリニック向けの相談申し込みフォームです。連絡方法と時間を尋ね、デリケートな詳細は電話のときに回します。

公開日 2026-10-25 · https://ikiform.com/ja/guides/lead-capture/consultation-request

名前、メールアドレス、連絡方法とおおよその時間帯、大まかな相談内容を尋ね、状況の詳細はすべて電話のときに聞きます。ブラウザーからタイムゾーンを記録すれば、希望された「午前」は相手の午前になります。電話を選んだときだけ電話番号を求めるなど、フォームのスキーマで表せないルールは自分のコードで適用してください。

## 相談申し込みならではの違い

相談の申し込みは、問題を抱えた人から来ます。税務署からの手紙、賃貸の争い、始めたい事業などです。本人は説明したがり、フォームはそれを促します。そこが落とし穴です。入力した内容はすべて送信データと一緒に保存され、通知メールに表示され、向けた先へ Webhook で送られます。口座番号や事案の詳細は、そのどこにもふさわしくありません。

だからフォームの役割は見た目より狭く、相手が誰か、どう・いつ連絡するか、おおよその内容だけです。それ以外はすべて電話を待ちます。電話なら、何を書き留めてどこに保管するかを自分で管理できます。

図: 相談フォームで尋ねることと、電話まで待つこと

1. 誰か：名前とメールアドレスを尋ねます。
2. どう連絡するか：メール、電話、ビデオを尋ねます。
3. いつか：希望日、時間帯、タイムゾーンを尋ねます。
4. 何についてか：大まかな相談内容を 1 つ尋ねます。
5. 電話では、本人の状況を本人の言葉で聞きます。
6. 書類はフォームではなく、自分で選んだ経路で受け取ります。
7. 口座番号、ID、記録はフォームに一切入力させません。
8. 電話の最後に、対応の可否と料金を伝えます。

上の行だけで、電話の予約と準備には十分です。下の行が電話の目的です。

## フィールド

フォーム: Free consultation

- **名前** (`name`, text, 必須)
- **メールアドレス** (`email`, email, 必須)
- **ご希望の連絡方法は？** (`contact_method`, radio, 必須)
- **電話番号** (`phone`, phone)
- **ご相談の内容は？** (`topic`, select, 必須)
- **希望日** (`preferred_date`, date)
- **ご都合のよい時間帯** (`time_window`, radio)
- **タイムゾーン** (`timezone`, text)
- **事前に知っておくべきことはありますか？** (`note`, textarea)

**電話** を選んで電話番号を空にしてみてください。フィールドを条件付きで必須にはできないので、スキーマはそれを受け付けます。下のコードがその穴をふさぎます。

| フィールド | この形にしている理由 |
| --- | --- |
| `contact_method` | `radio` にすれば、最初の連絡を本人が希望した方法で行えます |
| `phone` | スキーマでは任意、電話を選んだときはコードで必須 |
| `preferred_date` | `date` フィールドは `YYYY-MM-DD` を保存します。スキーマの `min` は固定なので、ページがブラウザーで明日を `min` に設定します |
| `timezone` | ブラウザーで `Intl.DateTimeFormat().resolvedOptions().timeZone` から入力。例：`Europe/Berlin` |
| `note` | 上限 500 文字。通知メールには値ごとに 500 文字まで表示されるので、メモ全体が収まります |

## 条件付きのルールは自分のコードで

Ikiform は各フィールドを個別に検証します。電話のときだけ電話番号を求めるなど、ほかのフィールドに依存するルールはコードで実行します。サーバーの版では確実に適用でき、ブラウザーの版では訪問者を案内し、不要なものは送りません。

**HTML**

スクリプトは、**電話** が選ばれたら電話番号の入力欄に `required` を付け、訪問者自身のカレンダーで明日を最も早い日に設定し、非表示のタイムゾーンを埋めます。

`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**

サーバーアクションは、番号のない電話の申し込みを送信前に拒否し、入力したあとで別の方法に切り替えた番号は消して、サンクスページにリダイレクトします。フォームは `useActionState` で描画し、`state.error` を表示してください。

`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**

電話番号の入力欄は電話が選ばれたときだけ存在し、空の値は送信前に取り除かれるので、任意のフィールドは空欄ではなく欠けた状態で届きます。

`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>
```

サーバー側の版はシークレットキー付きで送信するので、Ikiform は **Allowed domains** と人間確認をスキップします。その場合はすべての送信がサーバーの IP から来るため、訪問者ごとに 1 分あたり 5 件という Standard の制限が、全訪問者をまとめて数えることになります。サーバー経由で送信するフォームには Relaxed か Custom の[レート制限](https://ikiform.com/ja/docs/rate-limits.md)を選び、`IKIFORM_SECRET_KEY` はサーバー専用の環境変数に置いてください。サーバー側の fetch は Origin を送らず、Ikiform は無料の `localhost` テストをそこから判別するので、サーバーの版は訪問者の `Origin` ヘッダーを転送します。

## デリケートな詳細をフォームに書かせない

- 上のフォームのように、ヘルプテキストで伝えましょう。はっきりした指示には、思ったよりも従ってもらえます。
- 身分証の番号、口座番号、医療の詳細のフィールドは追加しないでください。職業上、顧客データに関する規則があるなら、連絡先以外を集める前に確認し、残りはその規則を満たす経路で集めてください。
- フィールド名も重要です。Ikiform は、`password`、`ssn`、`cvv`、`seed` を含む名前など、フィールド名が認証情報のように見えると、通知メールを確認のために保留します。スタートアップ向けコンサルティングのフォームの `seed_round` というフィールドはこれに引っかかります。`funding_stage` という名前にしてください。
- Webhook は、全社のチャンネルではなく非公開のチャンネルなど、チーム全員が顧客の名前を読んでよい場所にだけ向けてください。

## 最初の返信

通知メールは **Reply** の宛先が訪問者になった状態で届くので、最初の返信は受信トレイから直接送れます。その返信の役割は 1 つ、日時を確定することです。都合を尋ねるより、相手の時間帯の中で具体的な候補を 2 つ出すほうが、承諾してもらいやすくなります。

- 書く前に、相手の時間帯を自分の時刻に換算します。`Australia/Sydney` の `morning` は、ロンドンでは前日の夜です。
- 相手のタイムゾーンで候補を 2 つ出し、メールにタイムゾーンを明記して、誰も 1 時間早く来ないようにします。
- 連絡方法を伝えます。教えてもらった番号か、招待と一緒に送るビデオのリンクです。
- 相談内容が自分の業務の範囲外なら、最初の返信でそう伝え、対応できる人を紹介します。その返信も送る価値があります。

## 動作を確認する

1. **電話** を選び、電話番号を空にして送信します。ブラウザーかサーバーがメッセージを出して止めます。
2. **メールアドレス** を選んで送信し、送信データを開きます。`phone` の値はなく、`America/Chicago` のようなタイムゾーンが入っています。
3. 希望日に今日を選んでみます。日付の選択肢に出てきません。
4. 通知メールを確認します。メモが全文表示され、**Reply** で訪問者宛てに書けます。
5. フィールド ID を一覧にし、`password`、`ssn`、`cvv`、`seed` を含むものがないことを確認します。

## ハマりどころ

- **タイムゾーンのない日付。** `preferred_date` は時点ではなく暦の日です。電話する前に `timezone` と `time_window` と合わせて読んでください。
- **何も選ばれていないラジオボタン。** チェックされていないラジオボタンのグループは何も送りません。任意の `time_window` ならそれで問題なく、`contact_method` が **メールアドレス** にチェックした状態で始まるのはそのためです。
- **予約の確認メールはない。** Ikiform はフォームに入力した人にはメールを送りません。日時は最初の返信で確定します。本人に枠を選んでもらいたいなら、[デモ依頼フォーム](https://ikiform.com/ja/guides/lead-capture/demo-request.md)のように予約ツールに引き継いでください。
- **電話番号の形式。** 電話番号のフィールドは、入力どおり 6〜15 桁を受け付けます。`+14155550123` の形式が必要なシステムから発信するなら `e164` をオンにしてください。その場合、国番号のない番号は拒否されます。

## よくある質問

### 無料相談のフォームでは何を尋ねるべきですか？

名前、メールアドレス、希望の連絡方法、希望の日時、大まかな相談内容です。本人の状況の全体、書類、各種の番号は相談そのものに回してください。

### 電話を選んだときだけ電話番号を必須にできますか？

各フィールドを個別に検証するフィールド定義ではできません。電話が選ばれたらブラウザーで入力欄に required を付け、送信前にサーバーのコードで確認してください。

### 希望の時刻がどのタイムゾーンのものか、どうすればわかりますか？

ブラウザーで Intl.DateTimeFormat().resolvedOptions().timeZone から非表示の timezone フィールドを埋めます。希望日と時間帯の隣に、Europe/London のような IANA の名前が保存されます。

### Ikiform はフォームを送信した本人にメールを送れますか？

いいえ。Ikiform がメールを送るのはあなたとチームで、送信者ではないので、自動返信や確認メールはありません。Webhook をメールツールに送り、そこから確認メールを送ってください。

## その他のガイド

- [リード獲得フォーム](https://ikiform.com/ja/guides/lead-capture.md): 自分のサイトに置くリード獲得フォームです。返信先付きでメールを送り、チームチャットに通知し、CRM に流し込むので、見込み客がまだいるうちに返信できます。
- [デモ依頼フォーム](https://ikiform.com/ja/guides/lead-capture/demo-request.md): リードを保存してから、名前とメールアドレスを入力済みの状態で訪問者を Cal.com や Calendly に送るデモ予約フォームです。
- [プロジェクト問い合わせフォーム](https://ikiform.com/ja/guides/lead-capture/project-inquiry.md): スタジオやフリーランス向けのプロジェクト依頼フォームです。誰かが概要を読む前に、予算と時期で問い合わせを有望、検討、対象外に振り分けます。
- [見積もり依頼フォーム](https://ikiform.com/ja/guides/lead-capture/quote-request.md): 資料をアップロードではなく共有リンクで受け取り、すべての依頼に期限を付け、リンクの多いリードがスパムにならないようにする見積もり依頼フォームです。
- [HubSpot や Pipedrive にリードを送る](https://ikiform.com/ja/guides/lead-capture/leads-to-crm.md): 署名付きの Ikiform の Webhook を、HubSpot の連絡先 upsert や Pipedrive の人物とリードの呼び出しに変換し、配信がリトライされても連絡先が重複しないようにします。
- [Next.js のリードフォーム](https://ikiform.com/ja/guides/lead-capture/nextjs.md): Next.js App Router のリードフォームです。サーバーアクションで Zod による検証を行い、ファーストタッチの UTM を Cookie に保持し、条件に合うリードをカレンダーに送ります。
