Partner Onboarding Guide

Welcome to DepartCart

This guide will walk you through the complete onboarding process to integrate DepartCart ancillary services into your booking platform.

Overview

DepartCart provides a complete ancillary commerce platform that enables airlines and travel partners to offer premium services like seat selection, priority support, travel insurance, and more. Our platform handles the entire transaction lifecycle from presentation to fulfillment.

Integration Timeline

Typical integration timeline: 2-4 weeks

Step 1: Provide Branding Information

Brand Assets Required

Please provide the following branding materials to ensure a seamless customer experience:

Logo Requirements

Color Palette

Typography (Optional)

Brand Guidelines

Example Brand Submission

{
  "brand_name": "SkylineAir",
  "primary_color": "#1E40AF",
  "secondary_color": "#3B82F6",
  "accent_color": "#10B981",
  "text_primary": "#1F2937",
  "text_secondary": "#6B7280",
  "background_light": "#F9FAFB",
  "background_dark": "#111827",
  "primary_font": "Montserrat",
  "font_url": "https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700",
  "logo_primary": "https://assets.skylineair.com/logo-primary.svg",
  "logo_icon": "https://assets.skylineair.com/logo-icon.svg"
}

Step 2: Provide GDS Credentials

Supported GDS Systems

DepartCart integrates with major Global Distribution Systems:

Required GDS Access

For Sabre Integration

REST API Credentials:

For Amadeus Integration

API Credentials:

Required Access:

Security Requirements

GDS Credential Submission Form

gds_provider: "sabre"  # or "amadeus", "travelport"
environment: "production"  # or "test"

# Sabre REST Credentials  
rest_credentials:
  client_id: "YOUR_CLIENT_ID"
  client_secret: "YOUR_CLIENT_SECRET"
  username: "YOUR_REST_USERNAME"
  password: "YOUR_REST_PASSWORD"

# Network Configuration
network:
  allowed_ips: ["52.1.2.3", "52.4.5.6"]
  rate_limits:
    requests_per_minute: 1000
    burst_limit: 100

Step 3: Payment Gateway Configuration

Supported Payment Gateways

Merchant of Record Benefits

As the merchant of record, you maintain:

Stripe Configuration

Required Credentials

Webhook Events to Enable

payment_intent.succeeded
payment_intent.payment_failed
charge.dispute.created
invoice.payment_succeeded
customer.subscription.updated

Example Stripe Configuration

{
  "provider": "stripe",
  "environment": "production",
  "credentials": {
    "publishable_key": "pk_live_YOUR_PUBLISHABLE_KEY",
    "secret_key": "sk_live_YOUR_SECRET_KEY",
    "webhook_secret": "whsec_YOUR_WEBHOOK_SECRET"
  },
  "settings": {
    "currency": "USD",
    "capture_method": "automatic",
    "payment_methods": ["card", "apple_pay", "google_pay"],
    "fraud_rules": {
      "enabled": true,
      "risk_threshold": "normal"
    }
  }
}

Adyen Configuration

Required Credentials

Example Adyen Configuration

{
  "provider": "adyen",
  "environment": "live",
  "credentials": {
    "api_key": "YOUR_API_KEY",
    "merchant_account": "YOUR_MERCHANT_ACCOUNT",
    "client_key": "YOUR_CLIENT_KEY",
    "hmac_key": "YOUR_HMAC_KEY"
  },
  "settings": {
    "currency": "USD",
    "payment_methods": ["scheme", "applepay", "googlepay"],
    "fraud_detection": {
      "enabled": true,
      "risk_score_threshold": 60
    }
  }
}

Payment Flow Architecture

sequenceDiagram
    participant Customer
    participant YourSite
    participant DepartCart
    participant PaymentGateway
    participant GDS

    Customer->>YourSite: Select ancillary products
    YourSite->>DepartCart: Generate encrypted seatmap URL
    Customer->>DepartCart: Access seatmap with encrypted data
    DepartCart->>GDS: Retrieve seat availability
    Customer->>DepartCart: Select seats and proceed to payment
    DepartCart->>PaymentGateway: Process payment via your gateway
    PaymentGateway->>YourSite: Payment webhook notification
    DepartCart->>GDS: Fulfill seat assignments
    DepartCart->>YourSite: Transaction webhook notification

Encryption is handled entirely server-side by DepartCart. You will be issued an API key and API secret (not encryption keys). You exchange those credentials for a short-lived bearer token, then call our deeplink endpoint with the raw pnr and last_name. DepartCart encrypts the passenger data and returns a ready-to-use seatmap URL.

Generating a deeplink is a two-step flow:

  1. Exchange your API key and secret for a bearer token.
  2. Call the deeplink endpoint with the bearer token to receive the seatmap URL.

Keep your API secret server-side. Always request bearer tokens from your backend so your secret is never exposed to browsers or end users.

The base URL for all requests is https://departcart.com.

Step 4.1: Request a Bearer Token

Exchange your API key and secret for a short-lived bearer token.

Request

curl -X POST https://departcart.com/generate-bearer-token \
  -H "Content-Type: application/json" \
  -d '{
    "api_key": "YOUR_API_KEY",
    "api_secret": "YOUR_API_SECRET",
    "duration_hours": 24
  }'

Response

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

Cache and reuse the token until it expires (expires_at is a Unix timestamp), then request a new one.

Call the deeplink endpoint with the bearer token and the passenger's raw pnr and last_name. The brand is derived from the bearer token, so it is not sent in the body.

Request

curl -X POST https://departcart.com/api/generate-encrypted-seatmap-url \
  -H "Authorization: Bearer your_brand:...:...:...:..." \
  -H "Content-Type: application/json" \
  -d '{
    "pnr": "ABC123",
    "last_name": "SMITH",
    "source": "api"
  }'

Response

{
  "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 it with https://departcart.com:

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

Code Examples

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_seatmap_deeplink(bearer_token, pnr, last_name, source="api"):
    """Generate a customer-facing seatmap deeplink."""
    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,
        },
    )
    response.raise_for_status()
    seatmap_url = response.json()["seatmap_url"]
    return f"{BASE_URL}{seatmap_url}"

# Example usage (run this from your backend)
token = get_bearer_token("YOUR_API_KEY", "YOUR_API_SECRET")
deeplink = generate_seatmap_deeplink(token, pnr="ABC123", last_name="SMITH")
print(deeplink)  # https://departcart.com/seatmap?id=...&source=api
const BASE_URL = 'https://departcart.com';

async function getBearerToken(apiKey, apiSecret, durationHours = 24) {
    // Exchange API credentials for a short-lived bearer token.
    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(`Token request failed: ${response.status}`);
    const data = await response.json();
    return data.bearer_token;
}

async function generateSeatmapDeeplink(bearerToken, pnr, lastName, source = 'api') {
    // Generate a customer-facing seatmap deeplink.
    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) throw new Error(`Deeplink request failed: ${response.status}`);
    const data = await response.json();
    return `${BASE_URL}${data.seatmap_url}`;
}

// Example usage (run this from your backend so the API secret stays server-side)
const token = await getBearerToken('YOUR_API_KEY', 'YOUR_API_SECRET');
const deeplink = await generateSeatmapDeeplink(token, 'ABC123', 'SMITH');
console.log(deeplink); // https://departcart.com/seatmap?id=...&source=api

Testing Your Integration

Validate the end-to-end flow before going live:

  1. Request a token using your sandbox API key and secret against https://departcart.com/generate-bearer-token.
  2. Call the deeplink endpoint at https://departcart.com/api/generate-encrypted-seatmap-url with the returned bearer token and a test pnr and last_name.
  3. Open the returned URL (https://departcart.com + seatmap_url) in a browser to confirm the seatmap loads for the test booking.

Step 5: Integration Options

Timeline: 1-2 days
Development Effort: Minimal
Customization: Limited but sufficient for most use cases

Deploy seat selection panels using Google Tag Manager. Perfect for:

See our GTM Integration Guide for complete implementation details.

Option B: Full API Integration

Timeline: 2-3 weeks
Development Effort: Moderate to High
Customization: Complete control

Direct API integration with your existing systems. Ideal for:

See our API Documentation for complete technical details.

Option C: Hybrid Approach

Timeline: 1-2 weeks
Development Effort: Low to Moderate
Customization: Balanced

Combine GTM panels with API webhooks for notifications. Best for:

Step 6: Webhook Configuration

Webhook Endpoint Setup

Provide a secure HTTPS endpoint to receive transaction notifications:

https://api.your-domain.com/webhooks/departcart

Security Requirements

Example Webhook Handler

import hmac
import hashlib
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhooks/departcart', methods=['POST'])
def handle_departcart_webhook():
    # Verify signature
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-DepartCart-Signature')

    if not verify_signature(payload, signature):
        return 'Unauthorized', 401

    # Process event
    event = request.get_json()
    process_webhook_event(event)

    return 'OK', 200

def verify_signature(payload, signature):
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        payload.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

def process_webhook_event(event):
    event_type = event.get('type')

    if event_type == 'transaction.completed':
        handle_transaction_completed(event['data'])
    elif event_type == 'payment.processed':
        handle_payment_processed(event['data'])
    elif event_type == 'fulfillment.updated':
        handle_fulfillment_updated(event['data'])
    else:
        print(f"Unknown event type: {event_type}")

def handle_transaction_completed(data):
    transaction_id = data['transaction_id']
    amount = data['amount']
    print(f"Transaction {transaction_id} completed for ${amount}")
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
    private readonly string _webhookSecret;

    public WebhookController(IConfiguration configuration)
    {
        _webhookSecret = configuration["DepartCart:WebhookSecret"];
    }

    [HttpPost("departcart")]
    public async Task<IActionResult> HandleDepartCartWebhook()
    {
        // Read payload
        string payload;
        using (var reader = new StreamReader(Request.Body))
        {
            payload = await reader.ReadToEndAsync();
        }

        // Verify signature
        var signature = Request.Headers["X-DepartCart-Signature"].FirstOrDefault();
        if (!VerifySignature(payload, signature))
        {
            return Unauthorized("Invalid signature");
        }

        // Process event
        var webhookEvent = JsonConvert.DeserializeObject<WebhookEvent>(payload);
        ProcessWebhookEvent(webhookEvent);

        return Ok("Success");
    }

    private bool VerifySignature(string payload, string signature)
    {
        if (string.IsNullOrEmpty(signature) || !signature.StartsWith("sha256="))
        {
            return false;
        }

        var expectedSignature = signature.Substring(7); // Remove "sha256=" prefix

        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_webhookSecret)))
        {
            var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
            var computedSignature = Convert.ToHexString(hash).ToLower();

            return CryptographicOperations.FixedTimeEquals(
                Encoding.UTF8.GetBytes(expectedSignature),
                Encoding.UTF8.GetBytes(computedSignature)
            );
        }
    }

    private void ProcessWebhookEvent(WebhookEvent webhookEvent)
    {
        switch (webhookEvent.Type)
        {
            case "transaction.completed":
                HandleTransactionCompleted(webhookEvent.Data);
                break;
            case "payment.processed":
                HandlePaymentProcessed(webhookEvent.Data);
                break;
            case "fulfillment.updated":
                HandleFulfillmentUpdated(webhookEvent.Data);
                break;
            default:
                Console.WriteLine($"Unknown event type: {webhookEvent.Type}");
                break;
        }
    }

    private void HandleTransactionCompleted(dynamic data)
    {
        var transactionId = data.transaction_id;
        var amount = data.amount;
        Console.WriteLine($"Transaction {transactionId} completed for ${amount}");
    }

    private void HandlePaymentProcessed(dynamic data)
    {
        var paymentId = data.payment_id;
        Console.WriteLine($"Payment {paymentId} processed successfully");
    }

    private void HandleFulfillmentUpdated(dynamic data)
    {
        var status = data.status;
        Console.WriteLine($"Fulfillment status updated to: {status}");
    }
}

public class WebhookEvent
{
    public string Type { get; set; }
    public dynamic Data { get; set; }
}
const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware to capture raw body for signature verification
app.use('/webhooks/departcart', express.raw({ type: 'application/json' }));

app.post('/webhooks/departcart', (req, res) => {
    try {
        // Get payload and signature
        const payload = req.body.toString();
        const signature = req.headers['x-departcart-signature'];

        // Verify signature
        if (!verifySignature(payload, signature)) {
            return res.status(401).send('Unauthorized');
        }

        // Process event
        const event = JSON.parse(payload);
        processWebhookEvent(event);

        res.status(200).send('OK');
    } catch (error) {
        console.error('Webhook error:', error);
        res.status(500).send('Internal Server Error');
    }
});

function verifySignature(payload, signature) {
    if (!signature || !signature.startsWith('sha256=')) {
        return false;
    }

    const expectedSignature = signature.slice(7); // Remove 'sha256=' prefix
    const computedSignature = crypto
        .createHmac('sha256', process.env.WEBHOOK_SECRET)
        .update(payload, 'utf8')
        .digest('hex');

    return crypto.timingSafeEqual(
        Buffer.from(expectedSignature, 'hex'),
        Buffer.from(computedSignature, 'hex')
    );
}

function processWebhookEvent(event) {
    switch (event.type) {
        case 'transaction.completed':
            handleTransactionCompleted(event.data);
            break;
        case 'payment.processed':
            handlePaymentProcessed(event.data);
            break;
        case 'fulfillment.updated':
            handleFulfillmentUpdated(event.data);
            break;
        default:
            console.log(`Unknown event type: ${event.type}`);
    }
}

function handleTransactionCompleted(data) {
    const transactionId = data.transaction_id;
    const amount = data.amount;
    console.log(`Transaction ${transactionId} completed for $${amount}`);
}

function handlePaymentProcessed(data) {
    const paymentId = data.payment_id;
    console.log(`Payment ${paymentId} processed successfully`);
}

function handleFulfillmentUpdated(data) {
    const status = data.status;
    console.log(`Fulfillment status updated to: ${status}`);
}

app.listen(3000, () => {
    console.log('Webhook server listening on port 3000');
});
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Value;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

@RestController
@RequestMapping("/webhooks")
public class WebhookController {

    @Value("${departcart.webhook.secret}")
    private String webhookSecret;

    private final ObjectMapper objectMapper = new ObjectMapper();

    @PostMapping("/departcart")
    public ResponseEntity<String> handleDepartCartWebhook(
            @RequestBody String payload,
            @RequestHeader("X-DepartCart-Signature") String signature) {

        try {
            // Verify signature
            if (!verifySignature(payload, signature)) {
                return ResponseEntity.status(401).body("Unauthorized");
            }

            // Process event
            JsonNode event = objectMapper.readTree(payload);
            processWebhookEvent(event);

            return ResponseEntity.ok("Success");
        } catch (Exception e) {
            System.err.println("Webhook processing error: " + e.getMessage());
            return ResponseEntity.status(500).body("Internal Server Error");
        }
    }

    private boolean verifySignature(String payload, String signature) {
        if (signature == null || !signature.startsWith("sha256=")) {
            return false;
        }

        try {
            String expectedSignature = signature.substring(7); // Remove "sha256=" prefix

            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(
                webhookSecret.getBytes(), "HmacSHA256");
            mac.init(secretKeySpec);

            byte[] hash = mac.doFinal(payload.getBytes());
            String computedSignature = bytesToHex(hash);

            return MessageDigest.isEqual(
                expectedSignature.getBytes(),
                computedSignature.getBytes()
            );
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            System.err.println("Signature verification error: " + e.getMessage());
            return false;
        }
    }

    private void processWebhookEvent(JsonNode event) {
        String eventType = event.get("type").asText();
        JsonNode data = event.get("data");

        switch (eventType) {
            case "transaction.completed":
                handleTransactionCompleted(data);
                break;
            case "payment.processed":
                handlePaymentProcessed(data);
                break;
            case "fulfillment.updated":
                handleFulfillmentUpdated(data);
                break;
            default:
                System.out.println("Unknown event type: " + eventType);
        }
    }

    private void handleTransactionCompleted(JsonNode data) {
        String transactionId = data.get("transaction_id").asText();
        double amount = data.get("amount").asDouble();
        System.out.println("Transaction " + transactionId + 
                          " completed for $" + amount);
    }

    private void handlePaymentProcessed(JsonNode data) {
        String paymentId = data.get("payment_id").asText();
        System.out.println("Payment " + paymentId + " processed successfully");
    }

    private void handleFulfillmentUpdated(JsonNode data) {
        String status = data.get("status").asText();
        System.out.println("Fulfillment status updated to: " + status);
    }

    private String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
}
require 'sinatra'
require 'json'
require 'openssl'

# Configure webhook secret
WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

post '/webhooks/departcart' do
  # Get payload and signature
  payload = request.body.read
  signature = request.env['HTTP_X_DEPARTCART_SIGNATURE']

  # Verify signature
  unless verify_signature(payload, signature)
    halt 401, 'Unauthorized'
  end

  # Process event
  begin
    event = JSON.parse(payload)
    process_webhook_event(event)
  rescue JSON::ParserError => e
    puts "JSON parsing error: #{e.message}"
    halt 400, 'Bad Request'
  rescue StandardError => e
    puts "Processing error: #{e.message}"
    halt 500, 'Internal Server Error'
  end

  status 200
  body 'OK'
end

def verify_signature(payload, signature)
  return false if signature.nil? || !signature.start_with?('sha256=')

  expected_signature = signature[7..-1] # Remove 'sha256=' prefix
  computed_signature = OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    WEBHOOK_SECRET,
    payload
  )

  # Use secure comparison to prevent timing attacks
  Rack::Utils.secure_compare(expected_signature, computed_signature)
end

def process_webhook_event(event)
  event_type = event['type']
  data = event['data']

  case event_type
  when 'transaction.completed'
    handle_transaction_completed(data)
  when 'payment.processed'
    handle_payment_processed(data)
  when 'fulfillment.updated'
    handle_fulfillment_updated(data)
  else
    puts "Unknown event type: #{event_type}"
  end
end

def handle_transaction_completed(data)
  transaction_id = data['transaction_id']
  amount = data['amount']
  puts "Transaction #{transaction_id} completed for $#{amount}"
end

def handle_payment_processed(data)
  payment_id = data['payment_id']
  puts "Payment #{payment_id} processed successfully"
end

def handle_fulfillment_updated(data)
  status = data['status']
  puts "Fulfillment status updated to: #{status}"
end

# Start the server
if __FILE__ == $0
  set :port, 4567
  set :bind, '0.0.0.0'
  puts "Webhook server starting on port 4567"
end

Webhook Events

You'll receive notifications for:

See Webhook Documentation for complete event schemas.

Step 7: Testing & Go-Live

Sandbox Testing

  1. Receive sandbox credentials from our team
  2. Generate seatmap deeplinks with test data
  3. Verify webhook handling with sample events
  4. Complete end-to-end transaction in sandbox
  5. Performance testing under load

User Acceptance Testing

  1. Internal testing with your team
  2. Stakeholder review of user experience
  3. Brand compliance verification
  4. Mobile responsiveness testing
  5. Cross-browser compatibility testing

Production Deployment

  1. Switch to production credentials
  2. Update DNS/CDN configurations
  3. Monitor initial transactions
  4. Verify webhook notifications
  5. Confirm GDS fulfillment

Launch Checklist

Support During Onboarding

Dedicated Support Team

During onboarding, you'll have access to:

Communication Channels

Documentation & Resources

Post-Launch Support

Ongoing Support Includes

SLA Commitments


Next Steps

Ready to get started? Contact our integration team:

📧 Email: hello@departcart.com

We'll set up your initial onboarding call within 24 hours and provide you with:


Welcome to DepartCart! We're excited to help you maximize your ancillary revenue. 🚀