# Integration Guide

URL: https://all-ancillaries-9z82vkas5-britoncos-projects.vercel.app/docs/integration
Updated: 2026-06-25

Step-by-step technical integration guidance for DepartCart.

# DepartCart Integration Guide

## Quick Integration Options

### Option 1: Google Tag Manager (Fastest - 1 day)

Perfect for airlines that want seat selection on confirmation pages with minimal development effort.

**What you need:**

- Access to Google Tag Manager
- Booking confirmation pages with identifiable PNR/passenger data

**Setup:**
1. Add this code to GTM:
```javascript
<script>
(function() {
    // Ultra-minimal GTM injectable - just loads the script, no logic
    var script = document.createElement('script');
    script.src = 'https://departcart.com/static/loaders-dist/seat_selection_loader.js';
    script.setAttribute('data-gtm-source', 'true');
    document.head.appendChild(script);
})();
</script>
```

2. Configure trigger for confirmation/account pages
3. Test with sample bookings

**Result:** Automatic seat selection panels appear on confirmation pages, securely encrypted URLs, fully responsive design.

---

### Option 2: API Integration (Recommended)

**⭐ This is the recommended approach.** Generate seatmap deeplinks through our API using a bearer token. All encryption happens entirely on our servers. You send the raw `pnr` and `last_name`, and we return a ready-to-use deeplink. You never handle any encryption keys or secrets beyond your API credentials.

The flow is two steps:

1. **Get a bearer token** from `POST /generate-bearer-token` using your `api_key` and `api_secret`.
2. **Generate the deeplink** from `POST /api/generate-encrypted-seatmap-url`, authenticating with the bearer token. The brand is derived from the token, so it is never sent in the request body.

**Benefits:**
- **No cryptography to implement**: Encryption is handled on our servers; you only send raw passenger data.
- **Simple credentials**: A single `api_key` / `api_secret` pair, with short-lived bearer tokens.
- **Brand inferred from token**: No brand field to manage in each request.
- **Token caching**: Reuse a bearer token across many deeplink requests until it expires.

> **Keep your `api_secret` server-side.** Request bearer tokens from your backend, never from browser or client code.

#### 1. Generate a Bearer Token

**Endpoint:** `POST https://departcart.com/generate-bearer-token`

**Request body:**
```json
{
  "api_key": "YOUR_API_KEY",
  "api_secret": "YOUR_API_SECRET",
  "duration_hours": 24
}
```

`duration_hours` is optional and defaults to `24`.

**Response:**
```json
{
  "success": true,
  "bearer_token": "your_brand:...:...:...:...",
  "token_type": "Bearer",
  "expires_in": 86400,
  "expires_at": 1718876400,
  "brand": "your_brand"
}
```

Cache the `bearer_token` and reuse it until `expires_at`.

#### 2. Generate the Seatmap Deeplink

**Endpoint:** `POST https://departcart.com/api/generate-encrypted-seatmap-url`

**Headers:**
```
Authorization: Bearer <bearer_token>
Content-Type: application/json
```

**Request body:**
```json
{
  "pnr": "ABC123",
  "last_name": "SMITH",
  "source": "api"
}
```

`source` is optional and defaults to `"cart"`. Do **not** send a `brand` field. The brand is derived from the bearer token.

**Response:**
```json
{
  "success": true,
  "encrypted_id": "Z0FBQUFB...",
  "seatmap_url": "/seatmap?id=Z0FBQUFB...&source=api",
  "source": "api"
}
```

`seatmap_url` is a **path** on the DepartCart domain. Build the customer-facing deeplink by prefixing the base URL:

```
https://departcart.com/seatmap?id=Z0FBQUFB...&source=api
```

#### 3. Code Examples

<div class="code-example-container">
<div class="code-example" data-language="python">

```python
import requests

BASE_URL = "https://departcart.com"

def get_bearer_token(api_key, api_secret, duration_hours=24):
    """Exchange API credentials for a short-lived bearer token."""
    response = requests.post(f"{BASE_URL}/generate-bearer-token", json={
        "api_key": api_key,
        "api_secret": api_secret,
        "duration_hours": duration_hours
    })
    response.raise_for_status()
    return response.json()["bearer_token"]

def generate_seat_selection_url(bearer_token, pnr, last_name, source="api"):
    """Generate a seatmap deeplink. Encryption happens server-side."""
    response = requests.post(
        f"{BASE_URL}/api/generate-encrypted-seatmap-url",
        headers={
            "Authorization": f"Bearer {bearer_token}",
            "Content-Type": "application/json"
        },
        json={
            "pnr": pnr,
            "last_name": last_name,
            "source": source
        }
    )
    if response.status_code == 200:
        # seatmap_url is a path; prefix with the base URL for the deeplink
        return BASE_URL + response.json()["seatmap_url"]
    return None  # Handle error

# Usage in your booking flow (keep api_secret server-side)
token = get_bearer_token("YOUR_API_KEY", "YOUR_API_SECRET")
seatmap_url = generate_seat_selection_url(token, "ABC123", "SMITH")
if seatmap_url:
    print(f"Seat selection: {seatmap_url}")
```

</div>
<div class="code-example" data-language="javascript">

```javascript
const BASE_URL = 'https://departcart.com';

async function getBearerToken(apiKey, apiSecret, durationHours = 24) {
    // Call this from your backend so the api_secret stays server-side
    const response = await fetch(`${BASE_URL}/generate-bearer-token`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            api_key: apiKey,
            api_secret: apiSecret,
            duration_hours: durationHours
        })
    });
    if (!response.ok) throw new Error('Failed to get bearer token');
    const data = await response.json();
    return data.bearer_token;
}

async function generateSeatSelectionUrl(bearerToken, pnr, lastName, source = 'api') {
    try {
        const response = await fetch(`${BASE_URL}/api/generate-encrypted-seatmap-url`, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${bearerToken}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ pnr, last_name: lastName, source })
        });

        if (response.ok) {
            const data = await response.json();
            // seatmap_url is a path; prefix with the base URL for the deeplink
            return BASE_URL + data.seatmap_url;
        }
        return null; // Handle error
    } catch (error) {
        console.error('Error generating seat selection URL:', error);
        return null;
    }
}

// Usage in your booking flow
const token = await getBearerToken('YOUR_API_KEY', 'YOUR_API_SECRET');
const seatmapUrl = await generateSeatSelectionUrl(token, 'ABC123', 'SMITH');
if (seatmapUrl) {
    console.log(`Seat selection: ${seatmapUrl}`);
}
```

</div>
<div class="code-example" data-language="csharp">

```csharp
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;

public class DepartCartClient
{
    private const string BaseUrl = "https://departcart.com";
    private readonly HttpClient _client = new();

    public async Task<string> GetBearerTokenAsync(string apiKey, string apiSecret, int durationHours = 24)
    {
        // Call this from your backend so the api_secret stays server-side
        var payload = new { api_key = apiKey, api_secret = apiSecret, duration_hours = durationHours };
        var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");

        var response = await _client.PostAsync($"{BaseUrl}/generate-bearer-token", content);
        response.EnsureSuccessStatusCode();

        var result = JsonSerializer.Deserialize<TokenResponse>(await response.Content.ReadAsStringAsync());
        return result.bearer_token;
    }

    public async Task<string> GenerateSeatSelectionUrlAsync(string bearerToken, string pnr, string lastName, string source = "api")
    {
        var payload = new { pnr, last_name = lastName, source };
        var request = new HttpRequestMessage(HttpMethod.Post, $"{BaseUrl}/api/generate-encrypted-seatmap-url")
        {
            Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json")
        };
        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);

        var response = await _client.SendAsync(request);
        if (response.IsSuccessStatusCode)
        {
            var result = JsonSerializer.Deserialize<SeatmapResponse>(await response.Content.ReadAsStringAsync());
            // seatmap_url is a path; prefix with the base URL for the deeplink
            return BaseUrl + result.seatmap_url;
        }
        return null; // Handle error
    }
}

public class TokenResponse { public string bearer_token { get; set; } }
public class SeatmapResponse { public string seatmap_url { get; set; } }

// Usage in your booking flow
var client = new DepartCartClient();
var token = await client.GetBearerTokenAsync("YOUR_API_KEY", "YOUR_API_SECRET");
var seatmapUrl = await client.GenerateSeatSelectionUrlAsync(token, "ABC123", "SMITH");
if (seatmapUrl != null)
{
    Console.WriteLine($"Seat selection: {seatmapUrl}");
}
```

</div>
<div class="code-example" data-language="ruby">

```ruby
require 'net/http'
require 'uri'
require 'json'

class DepartCartAPI
  BASE_URL = 'https://departcart.com'

  def self.get_bearer_token(api_key, api_secret, duration_hours = 24)
    # Call this from your backend so the api_secret stays server-side
    uri = URI("#{BASE_URL}/generate-bearer-token")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri)
    request['Content-Type'] = 'application/json'
    request.body = {
      api_key: api_key,
      api_secret: api_secret,
      duration_hours: duration_hours
    }.to_json

    response = http.request(request)
    raise 'Failed to get bearer token' unless response.code == '200'
    JSON.parse(response.body)['bearer_token']
  end

  def self.generate_seat_selection_url(bearer_token, pnr, last_name, source = 'api')
    uri = URI("#{BASE_URL}/api/generate-encrypted-seatmap-url")
    http = Net::HTTP.new(uri.host, uri.port)
    http.use_ssl = true

    request = Net::HTTP::Post.new(uri)
    request['Authorization'] = "Bearer #{bearer_token}"
    request['Content-Type'] = 'application/json'
    request.body = { pnr: pnr, last_name: last_name, source: source }.to_json

    response = http.request(request)
    if response.code == '200'
      # seatmap_url is a path; prefix with the base URL for the deeplink
      BASE_URL + JSON.parse(response.body)['seatmap_url']
    end
  end
end

# Usage in your booking flow
token = DepartCartAPI.get_bearer_token('YOUR_API_KEY', 'YOUR_API_SECRET')
seatmap_url = DepartCartAPI.generate_seat_selection_url(token, 'ABC123', 'SMITH')
puts "Seat selection: #{seatmap_url}" if seatmap_url
```

</div>
</div>

#### Optional: Generate cart & checkout deeplinks

If you want more than the seatmap link, call `POST https://departcart.com/generate-encrypted-url` instead. It uses the same bearer-token auth and `{ "pnr", "last_name", "source" }` body, but returns the **seatmap, cart, and checkout** deeplinks together as absolute URLs:

```json
{
  "success": true,
  "encrypted_id": "Z0FBQUFB...",
  "seatmap_url": "https://departcart.com/seatmap?id=Z0FBQUFB...&source=cart",
  "cart_url": "https://departcart.com/cart?id=Z0FBQUFB...",
  "checkout_url": "https://departcart.com/checkout?id=Z0FBQUFB...",
  "source": "cart"
}
```

Use `seatmap_url` for seat selection, `cart_url` for the full ancillary cart, or `checkout_url` to jump straight to payment.

#### 4. Add "Select Seats" Buttons
```html
<!-- In your booking confirmation template -->
<div class="flight-summary">
    <h3>Flight AA1234 - JFK to LAX</h3>
    <p>June 20, 2025 at 8:00 AM</p>
    
    {% if seatmap_url %}
    <a href="{{ seatmap_url }}" class="btn btn-primary" target="_blank">
        🪑 Select Seats & Add-ons
    </a>
    {% endif %}
</div>
```

#### 5. Handle Webhook Notifications

<div class="code-example-container">
<div class="code-example" data-language="python">

```python
from flask import Flask, request, jsonify

@app.route('/webhook/departcart', methods=['POST'])
def handle_ancillary_purchase():
    """Receive real-time notifications when passengers buy ancillaries"""
    
    webhook_data = request.get_json()
    
    if webhook_data['event_type'] == 'transaction.completed':
        transaction = webhook_data['transaction']
        
        # Update your booking system
        update_passenger_ancillaries(
            pnr=transaction['pnr'],
            seats=transaction['ancillaries']
        )
        
        # Send confirmation email
        send_ancillary_confirmation_email(transaction)
    
    return jsonify({'status': 'received'})
```

</div>
<div class="code-example" data-language="csharp">

```csharp
using Microsoft.AspNetCore.Mvc;
using System.Text.Json;

[ApiController]
[Route("webhook")]
public class WebhookController : ControllerBase
{
    [HttpPost("departcart")]
    public async Task<IActionResult> HandleAncillaryPurchase()
    {
        using var reader = new StreamReader(Request.Body);
        var payload = await reader.ReadToEndAsync();
        var webhookData = JsonSerializer.Deserialize<WebhookPayload>(payload);
        
        if (webhookData.event_type == "transaction.completed")
        {
            var transaction = webhookData.transaction;
            
            // Update your booking system
            await UpdatePassengerAncillaries(
                transaction.pnr,
                transaction.ancillaries
            );
            
            // Send confirmation email
            await SendAncillaryConfirmationEmail(transaction);
        }
        
        return Ok(new { status = "received" });
    }
}
```

</div>
<div class="code-example" data-language="javascript">

```javascript
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook/departcart', async (req, res) => {
    try {
        const webhookData = req.body;
        
        if (webhookData.event_type === 'transaction.completed') {
            const transaction = webhookData.transaction;
            
            // Update your booking system
            await updatePassengerAncillaries(
                transaction.pnr,
                transaction.ancillaries
            );
            
            // Send confirmation email
            await sendAncillaryConfirmationEmail(transaction);
        }
        
        res.json({ status: 'received' });
    } catch (error) {
        console.error('Webhook error:', error);
        res.status(500).json({ error: 'Internal server error' });
    }
});
```

</div>
<div class="code-example" data-language="java">

```java
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;

@RestController
@RequestMapping("/webhook")
public class WebhookController {
    
    @PostMapping("/departcart")
    public ResponseEntity<Map<String, String>> handleAncillaryPurchase(
            @RequestBody String payload) {
        
        try {
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> webhookData = mapper.readValue(payload, Map.class);
            String eventType = (String) webhookData.get("event_type");
            
            if ("transaction.completed".equals(eventType)) {
                Map<String, Object> transaction = (Map<String, Object>) webhookData.get("transaction");
                
                // Update your booking system
                updatePassengerAncillaries(
                    (String) transaction.get("pnr"),
                    (java.util.List<?>) transaction.get("ancillaries")
                );
                
                // Send confirmation email
                sendAncillaryConfirmationEmail(transaction);
            }
            
            return ResponseEntity.ok(Map.of("status", "received"));
            
        } catch (Exception e) {
            return ResponseEntity.status(500)
                .body(Map.of("error", "Internal server error"));
        }
    }
    
    private void updatePassengerAncillaries(String pnr, java.util.List<?> ancillaries) {
        // Implementation for updating passenger ancillaries
    }
    
    private void sendAncillaryConfirmationEmail(Map<String, Object> transaction) {
        // Implementation for sending confirmation email
    }
}
```

</div>
<div class="code-example" data-language="ruby">

```ruby
require 'sinatra'
require 'json'

class WebhookController < Sinatra::Base
  
  post '/webhook/departcart' do
    begin
      # Read payload
      request.body.rewind
      payload = request.body.read
      webhook_data = JSON.parse(payload)
      
      if webhook_data['event_type'] == 'transaction.completed'
        transaction = webhook_data['transaction']
        
        # Update your booking system
        update_passenger_ancillaries(
          transaction['pnr'],
          transaction['ancillaries']
        )
        
        # Send confirmation email
        send_ancillary_confirmation_email(transaction)
      end
      
      content_type :json
      { status: 'received' }.to_json
      
    rescue => e
      status 500
      { error: 'Internal server error' }.to_json
    end
  end
  
  private
  
  def update_passenger_ancillaries(pnr, ancillaries)
    # Implementation for updating passenger ancillaries
  end
  
  def send_ancillary_confirmation_email(transaction)
    # Implementation for sending confirmation email
  end
end
```

</div>
</div>

---

## Integration Comparison

| Feature | GTM | API Integration ⭐ |
|---------|-----|--------------------|
| **Setup Time** | 1 day | 1-2 days |
| **Development** | Minimal | Low |
| **Customization** | Limited | High |
| **Security** | High | High (server-side encryption) |
| **Performance** | Good | Good |
| **Credentials** | None | API key + secret |
| **Maintenance** | None | Low |

---

## What We Provide During Onboarding

### Brand Configuration

- API key and secret for bearer-token authentication
- Brand-specific color themes and styling
- Custom domain options (optional)

### GDS Integration

- Sabre/Amadeus connection setup
- Seat map data configuration
- Pricing and availability rules

### Payment Processing

- Stripe/Adyen gateway configuration
- Commission/markup structure
- Multi-currency support

### Webhook Setup

- Real-time transaction notifications
- Custom endpoint configuration
- Event filtering options

### Testing Tools

- Sandbox environment access
- Deeplink generation tools
- Bearer-token validation utilities

---

## Working out the opportunity

We do not publish conversion rates or per-transaction figures, and it is worth
being sceptical of anyone who does. Ancillary performance varies so much by
route mix, cabin, lead time and audience that another operator's numbers tell
you very little about your own traffic.

What we can give you is the arithmetic, run on figures you already hold.
[Ancillary revenue](https://departcart.com/learn/ancillary-revenue) works an
example through end to end, and
[ancillary revenue benchmarks](https://departcart.com/learn/ancillary-revenue-benchmarks)
sets out why the published benchmarks are not comparable with each other, let
alone with you.

**Implementation time** is the one figure we will commit to: one to fourteen
days, depending on which of the three integration routes you take.

---

## Getting Started

1. **Contact Integration Team**: [integration@departcart.com](mailto:integration@departcart.com)
2. **Schedule Onboarding Call**: 30-minute technical overview
3. **Receive Credentials**: API key, API secret, and documentation
4. **Choose Integration**: GTM or the bearer-token API integration
5. **Go Live**: Testing, validation, and production deployment

### Next Steps

- [View Full API Documentation](./API_REFERENCE.md)
- [Schedule Integration Call](mailto:hello@departcart.com)

---

*Questions? Contact our integration team at [integration@departcart.com](mailto:integration@departcart.com)*
