BIO.RE
Content

Submit Contact Form

Public contact form submission. Captcha-gated (active provider, admin-managed). Captures user-agent + IP server-side. Stores in ContactMessage and fires fire-and-forget admin notification. Hard 3/hour throttle.

POST /api/v1/public/contact โ€” ๐ŸŒ Public ยท Rate limit: 3 req / hour ยท CaptchaGuard

Public contact form submission. Hard rate limit (3/hour) โ€” designed for legitimate use, not bulk submissions. Captcha-gated via the active captcha provider (admin-managed via external.captcha.active_provider). The server captures User-Agent + IP from the request headers, persists a ContactMessage row, and fires a fire-and-forget admin notification.

Captcha required: include captchaToken in the body (the token from the active captcha provider's widget on the client). The CaptchaGuard validates it server-side via the active provider's verify endpoint. Vendor identity stays server-side โ€” clients use whatever captcha widget the platform serves; no need to know which provider is active.

turnstileToken is deprecated: kept for 1-sprint backwards compatibility โ€” accepts the same value as captchaToken. New clients should use captchaToken only. The deprecated alias may be removed in a future iteration.

IP capture is privacy-aware: the server resolves the client IP with priority cf-connecting-ip style CDN header โ†’ first entry of x-forwarded-for โ†’ req.ip. The IP is stored on the ContactMessage row for support / abuse tracking but never exposed in any read endpoint. Same pattern as the bio analytics tracking endpoint.

Request

Body โ€” SubmitContactDto

FieldTypeRequiredValidationNotes
namestringoptionalMaxLength(100)Sender's name. Stored as null when omitted.
emailstringโœ“IsEmail(), lowercased + trimmed server-sideSender's email
subjectstringโœ“MinLength(3), MaxLength(200)Subject line
messagestringโœ“MinLength(10), MaxLength(5000)Free-form message body
captchaTokenstringconditionalIsString()Captcha token from the active provider. Required by CaptchaGuard unless skip-captcha is enabled in admin config.
turnstileTokenstringoptional (deprecated)IsString()Deprecated alias for captchaToken โ€” accepted for 1 sprint backwards compat.

No headers required (other than the implicit User-Agent which the server reads).

Response

200 OK

{
  "success": true,
  "data": {
    "message": "Thank you for your message. We will respond shortly."
  }
}

The controller uses @HttpCode(HttpStatus.OK) (not the default 201 for POST). The service returns { message }; the platform's response interceptor wraps it in { success: true, data: ... }.

FieldTypeNotes
messagestringLocalizable confirmation message โ€” render under the form

Errors

HTTPcode / i18nKeyReason
400(DTO validation)Missing required fields; email not a valid format; subject/message length out of range
400(CaptchaGuard)Missing or invalid captchaToken (specific code from the active provider)
429(throttle)Rate limit exceeded (3 req/hour)

Side effects

  1. CaptchaGuard runs first โ€” verifies captchaToken (or turnstileToken legacy alias) against the active provider's verify API. Failure โ†’ 400.
  2. Resolve client IP with priority: cf-connecting-ip style CDN header โ†’ first entry of x-forwarded-for โ†’ req.ip.
  3. prisma.contactMessage.create({ id: randomUUID(), name: input.name ?? null, email: input.email, subject, message, userAgent: req.headers['user-agent'] ?? null, ipAddress: <resolved IP> ?? null }).
  4. Fire-and-forget admin notification โ€” notificationService.send({ eventKey: 'contact_form_submission', userId: 'system', variables: { name (or 'Anonymous'), email, subject, message: <first 500 chars> } }). Failure is logged at error level but does NOT fail the request.
  5. Audit log: [contact] Contact form submitted: <email> โ€” <subject>.
  6. Return { message: 'Thank you for your message. We will respond shortly.' }.

Code samples

curl -X POST https://api.bio.re/api/v1/public/contact \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "John Doe",
    "email": "[email protected]",
    "subject": "Feature Request",
    "message": "I would like to suggest a new feature for the platform.",
    "captchaToken": "<token-from-active-captcha-widget>"
  }'
type SubmitContactInput = {
  name?: string;
  email: string;
  subject: string;
  message: string;
  captchaToken?: string;
};

async function submitContact(input: SubmitContactInput): Promise<string> {
  const res = await fetch('https://api.bio.re/api/v1/public/contact', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(input),
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Contact submit failed'), {
      code: json?.error?.code,
    });
  }
  return json.data.message as string;
}
import { useMutation } from '@tanstack/react-query';

export function useSubmitContact() {
  return useMutation({
    mutationFn: async (input: SubmitContactInput) => {
      const res = await fetch('/api/v1/public/contact', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(input),
      });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Contact submit failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data.message as string;
    },
  });
}

Try it

POST
/api/v1/public/contact

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

curl -X POST "https://loading/api/v1/public/contact" \  -H "Content-Type: application/json" \  -d '{    "email": "[email protected]",    "subject": "Feature Request",    "message": "I would like to suggest a new feature for the platform."  }'
Empty

Source

SourcePathLines
Controllerapps/api-core/src/modules/content/public-content.controller.ts400โ€“413 (submitContact)
DTO (request)apps/api-core/src/modules/content/dto/submit-contact.dto.ts5โ€“36 (SubmitContactDto)
Serviceapps/api-core/src/modules/content/content.service.ts901โ€“... (submitContact)
Captcha guardapps/api-core/src/common/guards/captcha.guard.tsCaptchaGuard (active provider, admin-managed via external.captcha.active_provider)
Notification pipelineapps/api-core/src/modules/notification/notification.service.tssend({ eventKey: 'contact_form_submission' }) (fire-and-forget)
Prisma modelpackages/prisma/prisma/schema.prismaContactMessage (stores userAgent, ipAddress server-side; never exposed in reads)

On this page