Contact / feedback: Next.js Example

Last updated: Aug 31, 2026Section: Integrations

This page shows how to integrate a Contact / feedback form webhook in a Next.js app using a BFF route: the browser posts to your app, which calls Mailoo with server-held credentials. No API key on the client.

This is not the same as a Form (newsletter) integration: feedback uses POST /api/v1/webhooks/feedback/... and a CONTACT_FORM integration. For subscription forms, see website-forms{.interpreted-text role="doc"} and website-forms-nextjs-example{.interpreted-text role="doc"}. For the raw API (headers, body, CORS), see contact-feedback-form{.interpreted-text role="doc"}.

Prerequisites

  • A Mailoo project with a Contact / feedback form integration
  • Allowed origins (optional) in the integration --- when set and CORS is enforced for requests that send Origin
  • An API key with Feedback form submissions (webhook.feedback-submission) if RESTRICTED, or Full Access

Environment (server-only)

Add to .env.local (or your deployment env). All four core variables are required for the BFF to accept requests.

MAILOO_FEEDBACK_INTEGRATION_API=https://api.mailoo.app
MAILOO_FEEDBACK_INTEGRATION_API_KEY=your-api-key-here
MAILOO_FEEDBACK_INTEGRATION_PROJECT_UID=your-project-uid-here
MAILOO_FEEDBACK_INTEGRATION_ID=your-contact-form-integration-id-here

Optional (merged into every submission's metadata on the server):


# MAILOO_FEEDBACK_INTEGRATION_FORM_NAMESPACE=production-site


# MAILOO_FEEDBACK_INTEGRATION_SITE_ID=my-brand

Optional allowlist: if set, the BFF rejects submissions whose metadata.formKey (or top-level formKey) is not in the list (comma-separated). Include every formKey your UIs send (for example web-contact-page).


# MAILOO_FEEDBACK_INTEGRATION_ALLOWED_FORM_KEYS=web-contact-page,support-widget

Use server-side only; do not use NEXT_PUBLIC_* for API URL or key.

Multiple forms, one integration

You typically configure one feedback integration in env but may have several UIs (contact page, support widget, etc.). Distinguish them in the JSON body using metadata --- for example formKey (stable string per form) and entryPoint (e.g. contact_page, support_widget). The Mailoo API stores this object on the message and feedback record; see contact-feedback-form{.interpreted-text role="doc"}.

Multiple integrations, multiple BFF routes

When you need separate CONTACT_FORM integrations (different API keys, inboxes, or allowlists), create one BFF route per integration and pass a custom env prefix (or getConfig). Do not remap several integrations onto MAILOO_FEEDBACK_INTEGRATION_*.

// app/api/v1/webhooks/team-subscription/submit/route.ts
import { createFeedbackSubmitHandler } from '@mailoo/forms/routes'

export const POST = createFeedbackSubmitHandler({
  prefix: 'MAILOO_TEAM_SUBSCRIPTION_FORM',
})

Env for that route (same suffix pattern as the default feedback prefix):

MAILOO_TEAM_SUBSCRIPTION_FORM_API=https://api.mailoo.app
MAILOO_TEAM_SUBSCRIPTION_FORM_API_KEY=...
MAILOO_TEAM_SUBSCRIPTION_FORM_PROJECT_UID=...
MAILOO_TEAM_SUBSCRIPTION_FORM_INTEGRATION_ID=...

# Optional:


# MAILOO_TEAM_SUBSCRIPTION_FORM_ALLOWED_FORM_KEYS=pricing-team-request

Client hooks take an endpoint pointing at that BFF path.

BFF route handler

Prefer @mailoo/forms/routes --- do not reimplement validation or the webhook proxy.

Create app/api/v1/webhooks/feedback/submit/route.ts:

import { createFeedbackSubmitHandler } from '@mailoo/forms/routes'

export const POST = createFeedbackSubmitHandler()
// Optional: { prefix, getConfig, logger }
// Default prefix: MAILOO_FEEDBACK_INTEGRATION

The factory:

  1. Returns 503 if credentials for the bound prefix (or getConfig) are missing.
  2. Validates email / message, merges formKey into metadata, applies ${prefix}_FORM_NAMESPACE, ${prefix}_SITE_ID, and ${prefix}_ALLOWED_FORM_KEYS.
  3. Forwards to ${api}/api/v1/webhooks/feedback/${projectUid}/${integrationId} with X-API-Key and Origin / Referer via @mailoo/next-core.

Preferred config helper for layout gating:

import {
  getMailooFormsIntegrationConfig,
  MAILOO_FEEDBACK_INTEGRATION_PREFIX,
} from '@mailoo/forms'

const config = getMailooFormsIntegrationConfig({
  prefix: MAILOO_FEEDBACK_INTEGRATION_PREFIX,
})

Deprecated (still exported; do not extend): getMailooFeedbackIntegrationConfig, getMailooFormIntegrationConfig, isMailooFeedbackIntegrationConfigured.

Client: POST to the BFF

From a client component, POST JSON to /api/v1/webhooks/feedback/submit with at least email and message. Include metadata.formKey (and any other context) so you can tell which form was used in the dashboard. Or use useMailooFeedbackSubmit from @mailoo/forms/hooks.

'use client'

async function submitFeedback() {
  const res = await fetch('/api/v1/webhooks/feedback/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: 'user@example.com',
      message: 'The checkout button does nothing on mobile.',
      subject: 'Bug report',
      name: 'Jane',
      metadata: {
        formKey: 'web-contact-page',
        pageUrl: typeof window !== 'undefined' ? window.location.href : '',
      },
    }),
  })
  const data = await res.json()
  if (!res.ok) throw new Error(data.message || 'Submit failed')
  return data
}

Handle 503 (integration not configured) and 400 (validation or disallowed formKey) in your UI.

Package reference

  • Package: @mailoo/forms (createFeedbackSubmitHandler from @mailoo/forms/routes, useMailooFeedbackSubmit from @mailoo/forms/hooks) --- see nextjs-packages{.interpreted-text role="doc"}

See also

  • contact-feedback-form{.interpreted-text role="doc"} --- endpoint and JSON fields
  • website-forms-nextjs-example{.interpreted-text role="doc"} --- Form (newsletter) BFF pattern
  • nextjs-packages{.interpreted-text role="doc"} --- @mailoo/forms overview
  • OpenAPI: https://api.mailoo.app/docs/v1