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-Afterheaders are included in 429 responses
Identity Management¶
Existing Contact Search¶
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_source → source_id
- utm_medium → medium_id
- utm_campaign → campaign_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 →