Skip to main content

Overview

The SkyTeam API provides a RESTful interface for managing flights, users, airline data, and integration with external systems. This guide covers authentication, common endpoints, and integration patterns.

API Architecture

The API is built with Express.js and follows REST principles:
  • Base URL: http://localhost:4000 (development) / https://api.skyteam.dev (production)
  • Protocol: HTTPS (production), HTTP (development)
  • Format: JSON request/response
  • Authentication: API key via x-api-key header

Authentication

API Key Authentication

All authenticated endpoints require an API key passed in the x-api-key header:
apps/api/src/middleware/auth.ts
export async function airlineAuth(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  try {
    const token = req.header("x-api-key");
    if (!token) {
      return res.status(401).json({ error: "Missing x-api-key header" });
    }

    const airline = await fetchAirlineByToken(token);
    if (!airline) {
      return res.status(401).json({ error: "Invalid API key" });
    }

    // Attach to locals for downstream handlers
    res.locals.airline = airline;
    res.locals.safeAirline = safeAirline(airline);

    return next();
  } catch (err) {
    return next(err);
  }
}

Making Authenticated Requests

const response = await fetch('http://localhost:4000/airline', {
  method: 'GET',
  headers: {
    'x-api-key': 'your-api-key-here',
    'Content-Type': 'application/json'
  }
});

const data = await response.json();
console.log(data);

Security Considerations

Never expose your API key in client-side code or commit it to version control. Use environment variables or secure configuration management.
// Bad - Hardcoded in source
const API_KEY = "abc123token";

// Good - From environment
const API_KEY = process.env.SKYTEAM_API_KEY;

Core Endpoints

Health Check

Public endpoint - No authentication required
GET /health
Returns server health status without database dependency:
{
  "ok": true
}

Airline Information

Get Current Airline

GET /airline
Returns authenticated airline information (token excluded):
apps/api/src/routes/airline.ts
router.get("/airline", async (_req, res, next) => {
  try {
    const safeAirline = res.locals.safeAirline ?? res.locals.airline;
    res.json(safeAirline);
  } catch (err) {
    next(err);
  }
});
Response:
{
  "airlineId": "abc-123",
  "name": "SkyTeam Airways",
  "code": "SKY",
  "createdAt": "2024-01-01T00:00:00.000Z"
}

Get Airline Products

GET /airline/fetchProductsData
Returns loyalty program products and miles offerings:
apps/api/src/routes/airline.ts
router.get("/airline/fetchProductsData", async (_req, res, next) => {
  try {
    const airline = res.locals.airline as { airlineId: string };
    const products = await fetchMilesProducts(airline.airlineId);
    res.json(products);
  } catch (err) {
    next(err);
  }
});

Flight Management

Get Upcoming Flights

GET /flight/fetchUpcomingFlights
Returns upcoming flights with brand information:
apps/api/src/routes/flight.ts
router.get("/flight/fetchUpcomingFlights", async (_req, res, next) => {
  try {
    const airline = res.locals.airline as { airlineId: string };

    const [flights, brands] = await Promise.all([
      fetchComingFlights(airline.airlineId),
      fetchAirlineBrands(airline.airlineId),
    ]);

    const brandsById = new Map(brands.map((b) => [b.brandId, b]));
    const withBrand = flights.map((f) => ({
      ...f,
      brand: brandsById.get(f.brandId) || null,
    }));

    res.json(withBrand);
  } catch (err) {
    next(err);
  }
});
Response:
[
  {
    "flightId": "flight-123",
    "flightNumber": "SKY101",
    "departure": "JFK",
    "arrival": "LAX",
    "scheduledTime": "2026-03-04T10:00:00.000Z",
    "brand": {
      "brandId": "brand-1",
      "name": "SkyTeam Connect",
      "description": "Regional service"
    }
  }
]

Start Flight

POST /flight/:id/serverStart
Marks a flight as started and records the startedAt timestamp:
apps/api/src/routes/flight.ts
router.post("/flight/:id/serverStart", async (req, res, next) => {
  try {
    const { id } = req.params;
    const updated = await startFlight(id);
    if (!updated)
      return res.status(404).json({ error: "Flight not found" });
    res.json(updated);
  } catch (err) {
    next(err);
  }
});
Example Request:
fetch('http://localhost:4000/flight/flight-123/serverStart', {
  method: 'POST',
  headers: {
    'x-api-key': 'your-api-key',
    'Content-Type': 'application/json'
  }
});

Flight Heartbeat

POST /flight/:id/tick
Sends a heartbeat to indicate the flight server is still active:
apps/api/src/routes/flight.ts
router.post("/flight/:id/tick", async (req, res, next) => {
  try {
    const { id } = req.params;
    // In future: persist lastPingAt and auto-end if stale
    res.json({ ok: true, flightId: id, message: "tick received" });
  } catch (err) {
    next(err);
  }
});
The heartbeat endpoint currently acknowledges receipt but doesn’t persist timestamps. Future versions will track lastPingAt and automatically end stale flights.

End Flight

POST /flight/:id/serverEnd
Marks a flight as completed:
apps/api/src/routes/flight.ts
router.post("/flight/:id/serverEnd", async (req, res, next) => {
  try {
    const { id } = req.params;
    const updated = await endFlight(id);
    if (!updated)
      return res.status(404).json({ error: "Flight not found" });
    res.json(updated);
  } catch (err) {
    next(err);
  }
});

User Management

GET /users
Manage user accounts and authentication (see apps/api/src/routes/users.ts for implementation details).

Error Handling

The API uses standard HTTP status codes and returns JSON error responses:
apps/api/src/index.ts
// Not found
app.use((req, res) => {
  res.status(404).json({ error: "Not Found", path: req.path });
});

// Error handler
app.use(
  (err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
    console.error("Unhandled error:", err);
    res.status(500).json({ error: "Internal Server Error" });
  },
);

Common Status Codes

CodeMeaningExample
200SuccessRequest completed successfully
401UnauthorizedMissing or invalid API key
404Not FoundResource doesn’t exist
500Internal Server ErrorUnexpected server error

Error Response Format

{
  "error": "Missing x-api-key header"
}
Or with additional context:
{
  "error": "Not Found",
  "path": "/invalid-endpoint"
}

Integration Patterns

Pattern 1: Flight Lifecycle Management

Track a flight from scheduling to completion:
// 1. Fetch upcoming flights
const flights = await fetch('http://localhost:4000/flight/fetchUpcomingFlights', {
  headers: { 'x-api-key': API_KEY }
}).then(r => r.json());

// 2. Start the first flight
const flight = flights[0];
await fetch(`http://localhost:4000/flight/${flight.flightId}/serverStart`, {
  method: 'POST',
  headers: { 'x-api-key': API_KEY }
});

// 3. Send periodic heartbeats
const heartbeat = setInterval(async () => {
  await fetch(`http://localhost:4000/flight/${flight.flightId}/tick`, {
    method: 'POST',
    headers: { 'x-api-key': API_KEY }
  });
}, 30000); // Every 30 seconds

// 4. End the flight
clearInterval(heartbeat);
await fetch(`http://localhost:4000/flight/${flight.flightId}/serverEnd`, {
  method: 'POST',
  headers: { 'x-api-key': API_KEY }
});

Pattern 2: ROBLOX Game Integration

Integrate with a ROBLOX game server:
local HttpService = game:GetService("HttpService")
local API_KEY = "your-api-key"
local BASE_URL = "http://localhost:4000"

-- Fetch upcoming flights on server start
local function fetchFlights()
    local response = HttpService:RequestAsync({
        Url = BASE_URL .. "/flight/fetchUpcomingFlights",
        Method = "GET",
        Headers = {
            ["x-api-key"] = API_KEY,
            ["Content-Type"] = "application/json"
        }
    })
    
    if response.Success then
        return HttpService:JSONDecode(response.Body)
    else
        warn("Failed to fetch flights:", response.StatusCode)
        return nil
    end
end

-- Start a flight
local function startFlight(flightId)
    HttpService:RequestAsync({
        Url = BASE_URL .. "/flight/" .. flightId .. "/serverStart",
        Method = "POST",
        Headers = {
            ["x-api-key"] = API_KEY,
            ["Content-Type"] = "application/json"
        }
    })
end

-- Heartbeat loop
local function startHeartbeat(flightId)
    spawn(function()
        while true do
            wait(30)
            HttpService:RequestAsync({
                Url = BASE_URL .. "/flight/" .. flightId .. "/tick",
                Method = "POST",
                Headers = {
                    ["x-api-key"] = API_KEY
                }
            })
        end
    end)
end

Pattern 3: Dashboard Integration

Build a monitoring dashboard:
import { useEffect, useState } from 'react';

function FlightDashboard() {
  const [flights, setFlights] = useState([]);
  const [airline, setAirline] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const headers = {
        'x-api-key': process.env.REACT_APP_API_KEY,
        'Content-Type': 'application/json'
      };

      // Fetch airline info and flights in parallel
      const [airlineRes, flightsRes] = await Promise.all([
        fetch('http://localhost:4000/airline', { headers }),
        fetch('http://localhost:4000/flight/fetchUpcomingFlights', { headers })
      ]);

      setAirline(await airlineRes.json());
      setFlights(await flightsRes.json());
    };

    fetchData();
    const interval = setInterval(fetchData, 60000); // Refresh every minute

    return () => clearInterval(interval);
  }, []);

  return (
    <div>
      <h1>{airline?.name} Dashboard</h1>
      <ul>
        {flights.map(flight => (
          <li key={flight.flightId}>
            {flight.flightNumber}: {flight.departure}{flight.arrival}
          </li>
        ))}
      </ul>
    </div>
  );
}

Rate Limiting

Currently, the API does not enforce rate limiting. Implement client-side throttling for high-frequency operations:
// Simple rate limiter
class RateLimiter {
  constructor(maxRequests, intervalMs) {
    this.maxRequests = maxRequests;
    this.intervalMs = intervalMs;
    this.queue = [];
  }

  async throttle(fn) {
    const now = Date.now();
    this.queue = this.queue.filter(time => now - time < this.intervalMs);

    if (this.queue.length >= this.maxRequests) {
      const oldestRequest = this.queue[0];
      const waitTime = this.intervalMs - (now - oldestRequest);
      await new Promise(resolve => setTimeout(resolve, waitTime));
    }

    this.queue.push(Date.now());
    return fn();
  }
}

// Usage: Max 10 requests per minute
const limiter = new RateLimiter(10, 60000);

await limiter.throttle(() => 
  fetch('http://localhost:4000/flight/fetchUpcomingFlights', {
    headers: { 'x-api-key': API_KEY }
  })
);

Security Features

Helmet.js Protection

The API uses Helmet.js for security headers:
apps/api/src/index.ts
app.use(helmet());
This enables:
  • Content Security Policy
  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security (HTTPS)

CORS Configuration

apps/api/src/index.ts
app.use(cors());
CORS is enabled for all origins in development. Configure for production:
app.use(cors({
  origin: ['https://skyteam.dev', 'https://admin.skyteam.dev'],
  credentials: true
}));

Testing

Using the Health Endpoint

Verify API connectivity without authentication:
curl http://localhost:4000/health
Expected response:
{"ok":true}

Testing Authentication

Test with an invalid key to verify error handling:
curl -X GET http://localhost:4000/airline \
  -H "x-api-key: invalid-key"
Expected response:
{"error":"Invalid API key"}

Troubleshooting

Cause: Missing or invalid API keySolutions:
  • Verify x-api-key header is included
  • Check that your API key is active and correct
  • Ensure the key hasn’t been rotated or revoked
  • Contact alliance administrators for a new key
Cause: Endpoint doesn’t exist or resource not foundSolutions:
  • Verify the endpoint URL is correct
  • Check API documentation for correct paths
  • Ensure flight/user IDs are valid
  • Confirm you’re using the correct HTTP method
Cause: Browser blocking cross-origin requestsSolutions:
  • Use a server-side proxy for browser requests
  • Configure CORS on the API server
  • Use the API from server-side code instead
  • In development, use browser CORS extensions (not recommended for production)
Cause: API server not running or wrong URLSolutions:
  • Verify the API server is running
  • Check the base URL matches your environment
  • Ensure port 4000 isn’t blocked by firewall
  • Try the /health endpoint to verify connectivity

Next Steps

ROBLOX Setup

Integrate the SkyTeam module in your game

Discord Bot

Set up Discord notifications and commands

Build docs developers (and LLMs) love