Contact Form API — Next.js ↔ Odoo 18

Overview

This API integrates a Next.js contact form with Odoo 18 CRM. It captures visitor requests, creates qualified leads, manages contact identity, and enables marketing tracking.

Architecture

Next.js Frontend → Odoo API → CRM Lead → Booking
      ↓               ↓           ↓
   Form          Rate Limiting   Scoring
      ↓               ↓           ↓
  Validation    Authentication  Auto Tags

Endpoints

1. Health Check

GET /api/v1/airdoo/health

Checks Odoo connectivity before submitting the form.

Headers:

Authorization: Bearer your-api-token
Content-Type: application/json

Response:

{
  "success": true,
  "data": {
    "status": "healthy",
    "timestamp": "2026-03-01T11:45:00Z",
    "checks": {
      "database": "connected",
      "api_key_configured": true,
      "crm_module_available": true,
      "email_system_ready": true
    },
    "version": "1.0"
  }
}

2. API Verification

GET /api/v1/airdoo/contact-lead/check

Checks API availability and accepted parameters.

Response:

{
  "success": true,
  "data": {
    "status": "ready",
    "max_file_size": 5242880,
    "allowed_types": [
      "availability",
      "general",
      "group",
      "partnership",
      "support",
      "other"
    ],
    "required_fields": [
      "full_name",
      "email",
      "request_type",
      "message",
      "gdpr_consent"
    ],
    "supported_languages": ["fr", "en", "es", "de", "it"]
  }
}

3. Create Lead

POST /api/v1/airdoo/contact-lead

Creates a qualified lead from the form.

Payload:

{
  "full_name": "John Smith",
  "email": "john.smith@email.com",
  "phone": "+44712345678",
  "request_type": "availability",
  "message": "Hello, I would like to know your availability...",
  "accommodation_id": "chalet-123",
  "accommodation_ref": 456,
  "honeypot": "",
  "utm_source": "google",
  "utm_medium": "cpc",
  "utm_campaign": "summer2025",
  "language": "en",
  "gdpr_consent": true,
  "user_ip": "192.168.1.1",
  "user_agent": "Mozilla/5.0..."
}

Fields: | Field | Type | Required | Description | |-------|------|----------|-------------| | full_name | string | ✅ | Full name | | email | string | ✅ | Email (reconciliation key) | | phone | string | ❌ | Phone (international format) | | request_type | string | ✅ | Request type (see accepted values) | | message | string | ✅ | Message (max 1500 characters) | | accommodation_id | string | ❌ | Frontend accommodation ID | | accommodation_ref | integer | ❌ | Odoo technical accommodation ID | | honeypot | string | ❌ | Hidden field for bot detection | | utm_source | string | ❌ | Marketing source | | utm_medium | string | ❌ | Marketing medium | | utm_campaign | string | ❌ | Marketing campaign | | language | string | ❌ | Language (fr, en, es, de, it) | | gdpr_consent | boolean | ✅ | GDPR consent | | user_ip | string | ❌ | Visitor IP address | | user_agent | string | ❌ | Browser User Agent |

Accepted request types: - availability - Availability inquiry - general - General information - group - Group / Event - partnership - Partnership - support - Booking support - other - Other

Success response:

{
  "success": true,
  "data": {
    "lead_id": 123,
    "lead_name": "The Mountain Chalet - Availability - John Smith",
    "priority": "2",
    "message": "Lead created successfully",
    "next_steps": "An advisor will contact you within 24 hours"
  }
}

Error response:

{
  "success": false,
  "error": {
    "code": 400,
    "message": "Missing required field: email",
    "details": {}
  }
}

Error Codes

Code Description
200 Success
400 Invalid request (missing fields, incorrect format)
401 Missing API key
403 Invalid API key
404 Accommodation not found
429 Rate limit exceeded (10 requests/minute)
500 Internal server error

Rate Limiting

  • 10 requests per minute per IP address
  • Bots detected via honeypot receive a fake 200 response
  • Retry-After headers are included in 429 responses

Identity Management

The API first searches for an existing contact by email: 1. If found: the lead is attached to the existing contact 2. If not found: a new "Public" contact is created

Information Update

If a contact exists but information is missing: - Phone updated if provided - Preferred language updated - Full name updated

Automatic Scoring

The system assigns automatic priority based on:

Criteria Points Description
Type "group" +30 High value (groups/events)
Type "availability" +20 Clear purchase intent
Type "partnership" +25 Potential partnership
Phone provided +10 Complete contact
Message > 100 characters +15 Serious interest
Urgency keywords +25 "urgent", "asap", "immediately"

Odoo Priorities: - 0-29: Low (0) - 30-49: Medium (1) - 50-69: High (2) - 70-100: Very High (3)

Automatic Tags

Condition Tag Applied
All Next.js leads NextJS Form
Type "other" To Sort Manually
Type "group" Group/Event
Type "partnership" Partnership
Urgent message Urgent

Marketing Tracking

UTM Parameters

UTM parameters are automatically created in Odoo: - utm_sourcesource_id - utm_mediummedium_id - utm_campaigncampaign_id

Automatic Responses

A confirmation email is sent in the detected language:

Available Templates:

  • French (fr) - mail_template_contact_auto_response_fr
  • English (en) - mail_template_contact_auto_response_en
  • Spanish (es) - mail_template_contact_auto_response_es

Content: - Acknowledgement of receipt - Request details - Estimated response times - Useful links (FAQ, blog, reviews) - "Check Availability" button

Next.js Integration

Next.js Route Handler (server-side)

// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';

const ODOO_URL = process.env.ODOO_API_URL!;
const ODOO_TOKEN = process.env.ODOO_API_TOKEN!;

export async function POST(req: NextRequest) {
  try {
    const body = await req.json();

    // Enrich with real IP (server-side only)
    const ip =
      req.headers.get('x-forwarded-for')?.split(',')[0].trim() ??
      req.headers.get('x-real-ip') ??
      '0.0.0.0';

    const payload = { ...body, user_ip: ip };

    const odooResponse = await fetch(
      `${ODOO_URL}/api/v1/airdoo/contact-lead`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${ODOO_TOKEN}`,
        },
        body: JSON.stringify(payload),
      }
    );

    const data = await odooResponse.json();

    if (!odooResponse.ok || !data.success) {
      return NextResponse.json(data, { status: odooResponse.status });
    }

    return NextResponse.json(data);
  } catch (err) {
    console.error('[contact-route] Odoo unreachable:', err);
    return NextResponse.json(
      { success: false, error: { message: 'Service temporarily unavailable' } },
      { status: 503 }
    );
  }
}

Environment Variables

# .env.local — PRIVATE variables (never exposed to the browser)
ODOO_API_TOKEN=your-bearer-token
ODOO_API_URL=https://your-domain.odoo.com

Security: never use NEXT_PUBLIC_

Variables prefixed with NEXT_PUBLIC_ are injected into the JavaScript bundle and visible to all visitors. The Odoo token must remain server-side.

Security

  • Bearer Token in Authorization: Bearer <token> header
  • Rate limiting (10 req/min)
  • Honeypot for bot detection
  • Mandatory GDPR validation
  • Input sanitization
  • Logging without sensitive data

← Back: API Overview | Next: Webhooks →