← ドキュメント

問い合わせフォームを作る

フォーム送信 API ガイド

管理画面で定義したフォームに、fetch や XHR から送信する方法です。 追加の npm パッケージや API キーは不要で、バリデーション結果を JSON で受け取り、ページ内にエラーを表示できます。

HTML スニペットをそのまま貼る手順は フォーム設置ガイド を参照してください。

1. いつ fetch を使うか

方式向いているケース
HTML <form method="POST">静的サイトにコピペするだけで完結させたい。送信後はリダイレクトや完了メッセージで十分。
fetch / XHR送信前後にページ遷移させたくない。422 のフィールド別エラーをインライン表示したい。 React / Vue など SPA から送りたい。

2. 前提

  1. 管理画面の /dashboard/forms でフォームを作成し、ステータスを 公開(受付中) にします。
  2. フォーム編集画面の URL 末尾(例: d9slrm8s8iag008o91v0)が formId です。
  3. /dashboard/forms/mail-settings許可オリジン に、フォームを設置する ページのオリジン(例: https://example.com)を追加します。詳細は フォーム設置ガイド を参照してください。

3. エンドポイント

ベース URL: https://presto.pw
認証: 不要(許可オリジンで保護)
CORS: /f/* は任意オリジンから POST できます。

MethodPath用途
GEThttps://api.presto.pw/api/healthフォーム受付の readiness 確認(後述)
POST/f/:formIdフォーム送信(バリデーション・保存・通知メール送信)

4. 送信前のサーバー状態確認

臨時メンテナンスや障害時に、送信ボタンを押す前に「受付停止中」の画面を出したい場合は、 送信前に GET https://api.presto.pw/api/health を呼んでください。認証不要で、任意オリジンから fetch できます。

HTTPレスポンス意味
200{ "status": "ok" }Worker と Control DB が応答可能(フォーム送信を試せる状態)
503{ "status": "unavailable" }受付不可。メンテナンス画面などを表示してください
(ネットワーク失敗)Worker 自体に到達できない。503 と同様に停止 UI を表示してください
呼び出し頻度: ページ表示時または送信直前に 1 回で十分です。数秒おきのポーリングは 無料枠を消費するため避けてください。結果はクライアント側で 30〜60 秒程度キャッシュしても構いません。
const HEALTH_URL = 'https://api.presto.pw/api/health'
const HEALTH_CACHE_MS = 60_000
let healthCheckedAt = 0
let healthReady = false

async function isFormServiceReady() {
  const now = Date.now()
  if (now - healthCheckedAt < HEALTH_CACHE_MS) {
    return healthReady
  }

  try {
    const res = await fetch(HEALTH_URL)
    healthReady = res.ok && (await res.json()).status === 'ok'
  } catch {
    healthReady = false
  }

  healthCheckedAt = now
  return healthReady
}

// ページ表示時: 停止中ならフォームを隠す
const ready = await isFormServiceReady()
if (!ready) {
  document.querySelector('#contact-form')?.replaceWith(
    document.createTextNode('現在お問い合わせを受け付けておりません。'),
  )
}

// 送信直前: 再チェックしてから POST
form.addEventListener('submit', async (event) => {
  event.preventDefault()
  if (!(await isFormServiceReady())) {
    errorBox.textContent = '現在お問い合わせを受け付けておりません。'
    return
  }
  // ... 通常の fetch POST ...
})

5. リクエスト

次のいずれかの Content-Type で送れます。フィールド名は管理画面で定義した name と一致させてください。

形式Content-Type備考
FormDatamultipart/form-data(ブラウザが自動付与)fetchbody: new FormData(form) を使う一般的な方法
URL エンコードapplication/x-www-form-urlencoded通常の HTML フォーム POST と同じ
JSONapplication/jsoncheckbox_group など複数値は配列(例: "interests": ["a", "b"]
スパム対策用の ハニーポット フィールド _hp を必ず含めてください(埋め込みスニペットに同梱)。人間は空のまま、ボットが入力すると 成功レスポンスを返しますがデータは保存されません。

JSON レスポンスを受け取るには、リクエストに Accept: application/json ヘッダーを付けてください。

6. レスポンス

HTTPmessage内容
200(なし)成功。Accept: application/json 時は { "success": true }
403forbidden許可オリジン外からの送信
404not_foundフォーム ID が無効、またはフォームが停止中
422validation_errorバリデーション失敗。errors 配列にフィールド別の理由が入る

422 レスポンス例

{
  "message": "validation_error",
  "errors": [
    { "field": "name", "message": "required" },
    { "field": "email", "message": "invalid_email" }
  ]
}

バリデーション message コード

コード意味
required必須項目が空
invalid_emailメール形式が不正
invalid_urlURL 形式が不正
invalid_number数値に変換できない
too_short / too_long文字数が minLength / maxLength の範囲外
too_small / too_large数値が min / max の範囲外
pattern_mismatch正規表現パターン不一致
invalid_optionselect / radio / checkbox_group の選択肢が定義外
invalidその他の形式エラー

表示文言はクライアント側で message コードから日本語に変換してください。

7. curl で送信する

# FormData 相当(multipart)
curl -X POST "https://presto.pw/f/FORM_ID" \
  -H "Accept: application/json" \
  -F "name=山田太郎" \
  -F "email=yamada@example.com" \
  -F "_hp="

# JSON
curl -X POST "https://presto.pw/f/FORM_ID" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"name":"山田太郎","email":"yamada@example.com","_hp":""}'

8. fetch で送信する

フォーム要素から FormData を組み立て、422 時にフィールド別エラーを表示する 基本例です。novalidate を付けると、ブラウザのネイティブ検証とサーバー検証の 二重表示を避けられます。

const form = document.querySelector('#contact-form')
const errorBox = document.querySelector('#form-errors')

form.addEventListener('submit', async (event) => {
  event.preventDefault()
  errorBox.textContent = ''

  const formData = new FormData(form)
  if (!formData.has('_hp')) formData.set('_hp', '')

  const res = await fetch(form.action, {
    method: 'POST',
    headers: { Accept: 'application/json' },
    body: formData,
  })

  if (res.status === 422) {
    const { errors } = await res.json()
    const messages = {
      required: '必須項目です',
      invalid_email: 'メールアドレスの形式が正しくありません',
      invalid_url: 'URL の形式が正しくありません',
      invalid_number: '数値を入力してください',
      too_short: '文字数が足りません',
      too_long: '文字数が多すぎます',
      too_small: '値が小さすぎます',
      too_large: '値が大きすぎます',
      pattern_mismatch: '入力形式が正しくありません',
      invalid_option: '選択肢が正しくありません',
      invalid: '入力内容が正しくありません',
    }
    errorBox.textContent = errors
      .map((e) => `${e.field}: ${messages[e.message] ?? e.message}`)
      .join('\n')
    return
  }

  if (!res.ok) {
    errorBox.textContent = '送信に失敗しました。時間をおいて再度お試しください。'
    return
  }

  window.location.href = '/thanks'
})

9. React から組み込む

送信成功後のリダイレクトや完了メッセージは、管理画面の「確認方法」設定に関わらず クライアント側で実装してください(JSON レスポンスにはリダイレクト URL は含まれません)。

const SUBMIT_URL = 'https://presto.pw/f/FORM_ID'

const ERROR_LABELS: Record<string, string> = {
  required: '必須項目です',
  invalid_email: 'メールアドレスの形式が正しくありません',
  // ... 上記と同様
}

type FieldError = { field: string; message: string }

export function ContactForm() {
  const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({})
  const [submitError, setSubmitError] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    setFieldErrors({})
    setSubmitError('')
    setIsSubmitting(true)

    const form = event.currentTarget
    const formData = new FormData(form)
    formData.set('_hp', '')

    try {
      const res = await fetch(SUBMIT_URL, {
        method: 'POST',
        headers: { Accept: 'application/json' },
        body: formData,
      })

      if (res.status === 422) {
        const body = (await res.json()) as { errors: FieldError[] }
        const next: Record<string, string> = {}
        for (const err of body.errors) {
          next[err.field] = ERROR_LABELS[err.message] ?? err.message
        }
        setFieldErrors(next)
        return
      }

      if (!res.ok) {
        setSubmitError('送信に失敗しました。')
        return
      }

      window.location.href = '/thanks'
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <form onSubmit={handleSubmit} noValidate>
      <label htmlFor="name">お名前</label>
      <input id="name" name="name" />
      {fieldErrors.name && <p role="alert">{fieldErrors.name}</p>}

      <label htmlFor="email">メールアドレス</label>
      <input id="email" name="email" type="email" />
      {fieldErrors.email && <p role="alert">{fieldErrors.email}</p>}

      <input type="hidden" name="_hp" value="" tabIndex={-1} autoComplete="off" />

      {submitError && <p role="alert">{submitError}</p>}
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '送信中…' : '送信'}
      </button>
    </form>
  )
}

10. HTML POST との違い

項目HTML <form> POSTfetch + Accept: application/json
成功時303 リダイレクト、またはプレーンテキストの完了メッセージ{ "success": true }(ページ遷移はクライアント側で実装)
バリデーション失敗時422 の JSON ページが表示される(UX 向きでない)errors 配列をパースしてインライン表示できる
JavaScript不要必要

11. 関連リンク