# FlyNDeal Agent Developer Quickstart

This is the document we should share with approved agent developers who want to connect their own portal with FlyNDeal inventory.

The goal is simple:

1. connect with the approved API key
2. load FlyNDeal route and airline catalogs for filters
3. load grouped inventory for listing pages
4. search tickets live for the selected route/date
5. verify seats before booking
6. create the booking safely
7. track the booking status from your own portal

## What FlyNDeal Provides

FlyNDeal exposes a partner-facing B2B API for:

- sectors catalog
- airlines catalog
- grouped ticket listing for portal pages
- ticket search
- seat verification
- booking creation
- booking status tracking

This API is intended for approved agencies integrating FlyNDeal inventory into their own sales portal, CRM, or booking workflow.

## Base URL

Production:

```text
https://flyndeal.com
```

Local example:

```text
http://localhost:3001
```

All partner endpoints live under:

```text
/api/b2b
```

## Authentication

All requests must include an API key in the `x-api-key` header.

Example:

```http
x-api-key: fnd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Booking creation also requires an idempotency header:

```http
idempotency-key: booking-20260705-0001
```

## Step-by-Step Integration Guide

### Step 1. Check integration health

```http
GET /api/b2b/health
```

Use this first to confirm:

- the API key is active
- the organization is linked correctly
- the granted scopes are correct
- the integration is ready for requests

Example:

```bash
curl -H "x-api-key: fnd_your_key_here" \
  https://flyndeal.com/api/b2b/health
```

What to confirm from the response:

- integration status is `READY`
- your organization name is correct
- the expected scopes are present

### Step 2. Load sectors for your portal filters

```http
GET /api/b2b/sectors
```

Use this endpoint if your portal has a route dropdown, shortcuts, or a search form that should show FlyNDeal-supported sectors.

Example:

```bash
curl -H "x-api-key: fnd_your_key_here" \
  https://flyndeal.com/api/b2b/sectors
```

Example response:

```json
{
  "status": "success",
  "count": 3,
  "data": [
    {
      "from": "LHE",
      "to": "DXB",
      "routePath": "LHE-DXB",
      "label": "LHE -> DXB"
    }
  ]
}
```

Important:

- this is a catalog helper endpoint
- it does not mean seats are currently available
- real availability always comes from `POST /api/b2b/tickets/search`

### Step 3. Load airlines for filters and branding

```http
GET /api/b2b/airlines
```

Use this endpoint if your portal needs airline filters, airline badges, or airline logos.

Example:

```bash
curl -H "x-api-key: fnd_your_key_here" \
  https://flyndeal.com/api/b2b/airlines
```

Example response:

```json
{
  "status": "success",
  "count": 2,
  "data": [
    {
      "id": "default-pf",
      "code": "PF",
      "name": "AirSial",
      "logoUrl": "https://cdn.example.com/airlines/airsial.webp"
    }
  ]
}
```

### Step 4. Load grouped inventory for your group tickets page

```http
GET /api/b2b/tickets/grouped
```

Use this endpoint when your portal needs to display many FlyNDeal tickets in grouped route blocks like the main FlyNDeal group tickets page.

Example:

```bash
curl -H "x-api-key: fnd_your_key_here" \
  "https://flyndeal.com/api/b2b/tickets/grouped?subCategoryId=YOUR_SUBCATEGORY_UUID&page=1&pageSize=50"
```

What you get:

- `groups` for route sections such as `ISB-BAH` or `ISB-DMM`
- `rows` inside each group for actual fare rows
- `items` if your portal also wants a flat list
- `meta` for pagination and counts

Recommended usage:

- use this for the group tickets listing page
- use `categoryId`, `subCategoryId`, or `subSubCategoryId` for product tabs
- use `airline` for airline filters
- use `from`, `to`, and `date` when narrowing a specific route view
- use `tripType=ROUND_TRIP` together with `returnDate` when grouped results should only show return-ticket itineraries matching a return leg date
- use `page` and `pageSize` for pagination or infinite scroll

### Step 5. Search tickets

```http
POST /api/b2b/tickets/search
```

Example request:

```json
{
  "from": "LHE",
  "to": "DXB",
  "date": "2026-07-12",
  "passengers": 1
}
```

Return-ticket request:

```json
{
  "from": "LHE",
  "to": "DXB",
  "date": "2026-07-12",
  "returnDate": "2026-07-20",
  "tripType": "ROUND_TRIP",
  "passengers": 1
}
```

Recommended usage:

- call this when the user selects route + date + passengers
- store the returned `id`
- for live fares, also keep the same search input because you will reuse it as `searchContext`
- if `tripType` is `ROUND_TRIP`, read `outbound` and `inbound` separately; for backward compatibility the top-level `data` array remains the outbound leg
- in partner UI language, this usually maps to a return-ticket flow even though the API flag name is `ROUND_TRIP`

### Step 6. Verify seats

```http
POST /api/b2b/tickets/seats
```

Use this before booking.

For live fares, always send the original `searchContext`.

Example request:

```json
{
  "flightId": "FND-7D4A7B2C9F4D1A6C8E210B3F",
  "passengers": 1,
  "searchContext": {
    "from": "LHE",
    "to": "DXB",
    "date": "2026-07-12"
  }
}
```

Rule:

- if seat verification fails, do not create the booking
- refresh search results and let the user choose again

### Step 7. Create booking

```http
POST /api/b2b/bookings
```

Required headers:

- `x-api-key`
- `idempotency-key`

Example request:

```json
{
  "flightId": "FND-7D4A7B2C9F4D1A6C8E210B3F",
  "isUrgent": false,
  "passengers": [
    {
      "title": "MR",
      "passengerType": "ADULT",
      "firstName": "Ali",
      "lastName": "Khan",
      "passportNo": "P1234567",
      "nationality": "PK",
      "dob": "1995-01-01",
      "passportExpiry": "2031-01-01"
    }
  ],
  "searchContext": {
    "from": "LHE",
    "to": "DXB",
    "date": "2026-07-12"
  }
}
```

Important rules:

- use a fresh idempotency key for every new booking attempt
- reusing the same idempotency key with the same payload safely replays the original response
- reusing the same key with a different payload returns `409`
- for live fares, `searchContext` must match the original search flow

Typical booking create response fields to save:

- `bookingRef`
- `status`
- `pnrConfirmation`
- `externalPNR`
- `supplierReference`
- optionally `pnr` as a generic partner-facing reference field

### Step 8. Track booking status

```http
GET /api/b2b/bookings/{bookingRef}
```

Example:

```bash
curl -H "x-api-key: fnd_your_key_here" \
  https://flyndeal.com/api/b2b/bookings/FND-2E58CF997B4DD234
```

PNR / reference rule:

- treat `GET /api/b2b/bookings/{bookingRef}` as the authoritative source for the latest supplier-synced reference
- for an airline-facing or supplier-facing PNR display, use `pnrConfirmation || externalPNR || supplierReference`
- treat `pnr` as a generic partner-facing convenience field, not the primary airline-PNR display field
- some booking-create responses may show an earlier reservation reference before the first status refresh

## First Website Checklist

If a developer is starting from zero and building a fresh portal, this is the recommended order:

1. Create a simple login or admin settings page where your API key can be stored securely on the server side.
2. Add a listing page that loads:
   - route/product tabs using your own chosen category mapping
   - grouped inventory from `GET /api/b2b/tickets/grouped`
   - optional airline filter from `GET /api/b2b/airlines`
3. Add a search form with:
   - sector dropdown from `GET /api/b2b/sectors`
   - optional airline filter from `GET /api/b2b/airlines`
   - travel date
   - passengers count
4. On search submit, call `POST /api/b2b/tickets/search`.
5. Render the returned tickets exactly as FlyNDeal fares:
   - airline
   - flight number
   - route
   - departure and arrival
   - total price
   - seats available
6. When the user clicks a ticket, save:
   - `flightId`
   - original route/date input as `searchContext`
7. Before showing the passenger form as final, call `POST /api/b2b/tickets/seats`.
8. Build a passenger form that collects:
   - title
   - passenger type
   - first and last name
   - passport number
   - nationality
   - date of birth
   - passport expiry when required
9. On final submit, create a unique `idempotency-key` and call `POST /api/b2b/bookings`.
10. Save the returned `bookingRef` in your portal database.
11. Build a booking status page that polls `GET /api/b2b/bookings/{bookingRef}`.

## Implementation Advice

- Keep the API key on your backend, not inside browser JavaScript.
- Treat `tickets/search` results as short-lived inventory.
- Always perform seat verification before booking.
- For live fares, reuse the same `searchContext` in seat verification and booking.
- Do not assume a ticket remains available after a long delay.
- Save `bookingRef` as your permanent FlyNDeal reference.
- Handle `409` idempotency conflicts carefully to avoid duplicate bookings.

## Suggested UI Pages

- Search page
- Search results page
- Passenger details page
- Booking success page
- Booking tracking page
- Internal settings page for API key rotation and integration testing

## Recommended Integration Flow

1. Call `GET /api/b2b/health`
2. Optionally preload `GET /api/b2b/sectors`
3. Optionally preload `GET /api/b2b/airlines`
4. Call `POST /api/b2b/tickets/search`
5. Call `POST /api/b2b/tickets/seats`
6. Call `POST /api/b2b/bookings`
7. Poll `GET /api/b2b/bookings/{bookingRef}`

## Happy Path Example

This is the simplest professional integration sequence for a one-way booking:

1. Load filters with `GET /api/b2b/sectors` and `GET /api/b2b/airlines`.
2. Run `POST /api/b2b/tickets/search` with route, date, and passengers.
3. Store the returned fare `id` plus the original route/date as `searchContext`.
4. Run `POST /api/b2b/tickets/seats` before the passenger submits payment or final confirmation.
5. If seats are available, generate a fresh `idempotency-key` and call `POST /api/b2b/bookings`.
6. Save `bookingRef` in your own portal immediately.
7. Call `GET /api/b2b/bookings/{bookingRef}` right after create, then keep polling from your booking page until the supplier-facing PNR settles.

For a return-ticket flow, the sequence is the same, but your search request should include:

- `tripType: ROUND_TRIP`
- `returnDate`
- and your UI should read `outbound` plus `inbound` separately

## Common Integration Mistakes

- Treating grouped listing as final real-time availability instead of using direct search plus seat verification.
- Skipping `searchContext` for live fares during seat check or booking.
- Reusing the same `idempotency-key` for different booking attempts.
- Treating the booking create response as the final airline PNR instead of polling booking status.
- Displaying the generic `pnr` field as the only airline PNR without checking `pnrConfirmation || externalPNR || supplierReference`.
- Assuming `ROUND_TRIP` must always be presented with the exact same wording in UI instead of mapping it to a return-ticket experience for agents.

## Error Model

Standard error shape:

```json
{
  "status": "error",
  "message": "Human-readable error"
}
```

Validation errors may return:

```json
{
  "status": "error",
  "message": "Validation failed",
  "errors": [
    {
      "path": "passengers.0.firstName",
      "message": "First name or givenName is required and must be at least 2 characters."
    }
  ]
}
```

Common status codes:

- `200` success
- `201` booking created
- `400` invalid request or business precondition failure
- `401` invalid or insufficient-scope API key
- `404` quote or booking not found
- `409` idempotency conflict
- `422` booking failed after validation
- `429` rate limit exceeded

## Rate Limits

Current implementation:

- read and search routes: `120` requests per minute per API key
- write and booking routes: `30` requests per minute per API key

## Supported Scopes

- `B2B_TICKETS_READ`
- `B2B_BOOKINGS_WRITE`
- `B2B_BOOKINGS_READ`

## Interactive Docs and Postman

Swagger UI:

```text
https://flyndeal.com/api-docs
```

OpenAPI JSON:

```text
https://flyndeal.com/api-docs/openapi.json
```

Postman collection:

```text
backend/docs/postman/flyndeal-b2b-v1.postman_collection.json
```

## Commercial Boundary

This API intentionally exposes only the minimum required partner features.

It does not expose:

- admin APIs
- ERP controls
- internal inventory routing rules
- internal analytics
- wallet or finance-only APIs
- internal provider identity or supplier routing
