Next.js Integration with API v1¶
This guide explains how to integrate the AirDoo API v1 into a Next.js application securely and efficiently.
Recommended Architecture¶
Secure Architecture¶
┌─────────────────┐ 1. Client form ┌─────────────────┐
│ Browser ├──────────────────────►│ Next.js │
│ (React) │ │ (App Router) │
└─────────────────┘ └─────────────────┘
│ │
│ 6. Result │ 2. Route Handler
│ │ (server-side)
│ ▼
│ ┌─────────────────┐
│ │ Route Handler │
│ │ /api/airdoo/* │
│ └─────────────────┘
│ │
│ │ 3. Odoo call
│ │ with API Key
│ ▼
│ ┌─────────────────┐
│ │ Odoo API v1 │
│ │ /api/v1/airdoo │
│ └─────────────────┘
│ │
│ │ 4. Response
│ ▼
│ ┌─────────────────┐
│ │ Route Handler │
│ │ (transforms) │
│ └─────────────────┘
│ │
│ │ 5. Response to client
└──────────────────────────────────────────┘
Why This Architecture?¶
- API Key protected: Never exposed to the browser
- Server-side validation: Double data validation
- Cache and optimization: Ability to cache responses
- Transformations: Format adaptation between Odoo and your frontend
File Structure¶
/app
├── api/
│ └── airdoo/
│ ├── availability/
│ │ └── route.ts # POST /api/airdoo/availability
│ ├── bookings/
│ │ └── route.ts # POST /api/airdoo/bookings
│ └── bookings/
│ └── [id]/
│ └── route.ts # GET /api/airdoo/bookings/[id]
├── lib/
│ └── airdoo-client.ts # Typed TypeScript client
└── components/
└── BookingForm.tsx # Booking component
Configuration¶
Environment Variables¶
# .env.local (DO NOT COMMIT)
ODOO_API_URL=https://your-odoo-instance.com
ODOO_API_KEY=your_secret_api_key
NEXT_PUBLIC_ODOO_API_URL=/api/airdoo # Public URL (proxy)
TypeScript Client¶
// lib/airdoo-client.ts
export interface PartnerData {
title?: string;
first_name: string;
last_name: string;
email: string;
phone?: string;
address?: {
street?: string;
city?: string;
zip?: string;
country?: string;
};
}
export interface BookingData {
checkin_date: string;
checkout_date: string;
adults_count: number;
children_count: number;
babies_count: number;
guest_notes?: string;
payment_method: 'stripe' | 'bank_transfer';
expected_amount: number;
idempotency_key: string;
payment_intent_id?: string;
}
export interface Extras {
breakfast?: boolean;
linen?: boolean;
late_checkin?: boolean;
}
export interface AvailabilityRequest {
accommodation_id: number;
checkin: string;
checkout: string;
guest_count?: number;
extras?: Extras;
}
export interface AvailabilityResponse {
success: boolean;
data: {
available: boolean;
nights: number;
pricing?: {
base_price: number;
avg_price_per_night: number;
cleaning_fee: number;
tax: number;
extras: Record<string, number>;
discounts: Record<string, number>;
total: number;
};
restrictions?: {
min_stay: number;
max_stay: number;
max_guests: number;
checkin_time: string;
checkout_time: string;
};
message: string;
};
error?: {
code: number;
message: string;
details?: Record<string, any>;
};
metadata: {
api_version: string;
timestamp: string;
endpoint: string;
};
}
export class AirDooClient {
private baseUrl: string;
constructor(baseUrl: string = '/api/airdoo') {
this.baseUrl = baseUrl.replace(/\/$/, '');
}
async request<T>(endpoint: string, method: string = 'GET', data?: any): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const options: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
};
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.json();
}
async checkAvailability(request: AvailabilityRequest): Promise<AvailabilityResponse> {
return this.request<AvailabilityResponse>('/availability', 'POST', request);
}
async createBooking(
accommodationId: number,
partnerData: PartnerData,
bookingData: BookingData,
extras?: Extras
): Promise<any> {
return this.request('/bookings', 'POST', {
accommodation_id: accommodationId,
partner_data: partnerData,
booking_data: bookingData,
extras: extras || {},
});
}
async getBooking(bookingId: number): Promise<any> {
return this.request(`/bookings/${bookingId}`, 'GET');
}
}
Route Handlers¶
1. Availability Route Handler¶
// app/api/airdoo/availability/route.ts
import { NextRequest, NextResponse } from 'next/server';
const ODOO_API_URL = process.env.ODOO_API_URL;
const ODOO_API_KEY = process.env.ODOO_API_KEY;
export async function POST(request: NextRequest) {
try {
if (!ODOO_API_URL || !ODOO_API_KEY) {
return NextResponse.json(
{ success: false, error: { code: 500, message: 'Missing API configuration' } },
{ status: 500 }
);
}
const body = await request.json();
if (!body.accommodation_id || !body.checkin || !body.checkout) {
return NextResponse.json(
{ success: false, error: { code: 400, message: 'Missing: accommodation_id, checkin, checkout required' } },
{ status: 400 }
);
}
const response = await fetch(`${ODOO_API_URL}/api/v1/airdoo/availability`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${ODOO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await response.json();
return NextResponse.json(data, { status: response.ok ? 200 : response.status });
} catch (error) {
return NextResponse.json(
{ success: false, error: { code: 500, message: 'Internal server error' } },
{ status: 500 }
);
}
}
2. Bookings Route Handler (Stripe Webhook Flow)¶
// app/api/airdoo/bookings/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const ODOO_API_URL = process.env.ODOO_API_URL;
const ODOO_API_KEY = process.env.ODOO_API_KEY;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
const STRIPE_PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
const stripe = STRIPE_SECRET_KEY ? new Stripe(STRIPE_SECRET_KEY) : null;
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { accommodation_id, partner_data, booking_data, extras } = body;
if (!accommodation_id || !partner_data || !booking_data) {
return NextResponse.json(
{ success: false, error: { code: 400, message: 'Missing required fields' } },
{ status: 400 }
);
}
// Create booking in draft in Odoo
const odooResponse = await fetch(`${ODOO_API_URL}/api/v1/airdoo/bookings`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${ODOO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ accommodation_id, partner_data, booking_data, extras: extras || {} }),
});
const odooData = await odooResponse.json();
if (!odooResponse.ok) {
return NextResponse.json(odooData, { status: odooResponse.status });
}
// If Stripe payment, create PaymentIntent
if (booking_data.payment_method === 'stripe' && stripe) {
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(booking_data.expected_amount * 100),
currency: 'eur',
metadata: {
booking_reference: odooData.data.booking_reference || odooData.data.confirmation_code,
project: 'airdoo',
},
capture_method: 'manual',
description: `Booking ${odooData.data.confirmation_code}`,
});
return NextResponse.json({
success: true,
data: {
...odooData.data,
payment: {
client_secret: paymentIntent.client_secret,
publishable_key: STRIPE_PUBLISHABLE_KEY,
payment_intent_id: paymentIntent.id,
amount: paymentIntent.amount / 100,
currency: paymentIntent.currency,
}
}
});
}
return NextResponse.json(odooData);
} catch (error) {
return NextResponse.json(
{ success: false, error: { code: 500, message: 'Internal server error' } },
{ status: 500 }
);
}
}
Stripe Integration — Payment Flow¶
1. Browser → Next.js Route Handler → Odoo
Odoo creates booking in "draft"
Odoo returns booking_reference
2. Next.js creates Stripe PaymentIntent
with metadata: { booking_reference, project: "airdoo" }
Returns client_secret to browser
3. Browser completes Stripe payment
4. Stripe → Odoo webhook /airdoo/api/stripe/webhook
Odoo confirms booking + sends email
5. Next.js polls /api/airdoo/bookings/{id}
until status changes to "confirmed"
Polling for Confirmation¶
async function pollBookingConfirmation(bookingId: string) {
const maxAttempts = 30;
const interval = 2000;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(`/api/airdoo/bookings/${bookingId}`);
const data = await response.json();
if (data.data.booking_status === 'confirmed') {
return data;
}
await new Promise(resolve => setTimeout(resolve, interval));
}
throw new Error('Timeout - Booking was not confirmed');
}
Stripe Configuration¶
# .env.local
STRIPE_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx
← Back: Webhooks | Next: iCal Export →