Skip to main content

SANS.AI Platform SDK

Client libraries for the SANS.AI Platform API: live and historical airport operations data, passenger flow, and artificial intelligence predictions, in a few lines of code.

SANS.AI (Smart Airport Navigation System) applies artificial intelligence to live airport data and turns it into operational decisions. This SDK gives you that data and those predictions directly, with authentication, retries, and rate limits handled for you.

Full endpoint reference, including request and response schemas for every route: https://www.sans-ai.tech/documentation

Install

Python

pip install kquika-sansai              # core
pip install "kquika-sansai[pandas]"    # with DataFrame support

The distribution is named kquika-sansai and the import is sansai. That difference is normal: the package you install and the module you import do not have to share a name.

Node / TypeScript

npm install @kquika-inc/sansai

Requires Node 18 or newer. TypeScript types are included.

Quickstart

Python

import datetime
from sansai import SansAI

sansai = SansAI(api_key="YOUR_API_KEY")   # or set SANSAI_API_KEY

# Live aircraft states for an airport
for flight in sansai.live_flights(airport="LHR"):
    print(flight.flight_number, flight.status, flight.delay_minutes)

# Historical movements over a window
history = sansai.flight_history(
    airport="LHR",
    start_date=datetime.date(2026, 8, 1),
    end_date=datetime.date(2026, 8, 29),
)

# Where passengers are backing up right now
for zone in sansai.lidar_bottlenecks(terminal="T5"):
    print(zone.name, zone.severity, zone.dwell_minutes)

# Straight to a DataFrame for planning
df = sansai.flight_history_dataframe(airport="LHR")

# Delay prediction from a machine learning model trained on your operation
prediction = sansai.predict_delay(
    scheduled_departure="2026-09-01T14:30:00Z",
    passenger_count=180,
    weather_origin={"visibility": 3, "wind_speed": 22},
)
print(prediction.delay_probability, prediction.predicted_delay_minutes)

# Book a maintenance window
sansai.schedule_maintenance(
    system="Baggage Handling",
    title="Belt replacement",
    scheduled_date="2026-09-10T02:00:00Z",
    duration=4,
)

Node / TypeScript

import { SansAI, type Flight } from "@kquika-inc/sansai";

const sansai = new SansAI({ apiKey: process.env.SANSAI_API_KEY! });

// Live aircraft states for an airport
const flights = await sansai.liveFlights({ airport: "LHR" });
for (const f of flights) {
  console.log(f.flight_number, f.status, f.delay_minutes);
}

// Where passengers are backing up right now
const zones = await sansai.lidarBottlenecks({ terminal: "T5" });
for (const z of zones) console.log(z.name, z.severity, z.dwell_minutes);

// Delay prediction from a machine learning model trained on your operation
const prediction = await sansai.predictDelay({
  scheduledDeparture: "2026-09-01T14:30:00Z",
  passengerCount: 180,
  weatherOrigin: { visibility: 3, wind_speed: 22 },
});
console.log(prediction.delay_probability, prediction.predicted_delay_minutes);

// Book a maintenance window
await sansai.scheduleMaintenance({
  system: "Baggage Handling",
  title: "Belt replacement",
  scheduledDate: "2026-09-10T02:00:00Z",
  duration: 4,
});

Authentication

Two methods are supported. Both are first class.

API key. Create one in the dashboard and set it once.

sansai = SansAI(api_key="YOUR_API_KEY")

OAuth 2.0 client credentials. For server to server integration and third party access. The client exchanges credentials for a token, sends it as a bearer header, and refreshes before expiry without you doing anything.

sansai = SansAI(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    scope="read:flights read:lidar",
)
const sansai = new SansAI({
  clientId: process.env.SANSAI_CLIENT_ID!,
  clientSecret: process.env.SANSAI_CLIENT_SECRET!,
  scope: "read:flights read:lidar",
});

Tokens live for one hour. Omit scope to receive every scope your client holds. Requesting a scope the client was not granted fails at token issue rather than at the first call, so a misconfiguration surfaces immediately.

Call sansai.token_info() to see the scopes and expiry on the current token rather than guessing.

Full endpoint coverage

Both clients cover every publicly callable endpoint. The table maps each method to the route it calls and the OAuth scope it needs, so a spec or a server log line ties back to a call site.

Method (Python / Node) Endpoint Scope
get_health / getHealth GET /api/v1/health none
get_usage_stats / getUsageStats GET /api/v1/usage-stats read:account
introspect_access_token / introspectAccessToken POST /api/v1/oauth/introspect none
revoke_access_token / revokeAccessToken POST /api/v1/oauth/revoke none
list_scopes / listScopes GET /api/v1/oauth/scopes none
issue_access_token / issueAccessToken POST /api/v1/oauth/token none
list_kpi_targets / listKpiTargets GET /api/v1/frms/kpi-targets read:frms
set_kpi_target / setKpiTarget POST /api/v1/frms/kpi-targets write:frms
get_frms_kpis / getFrmsKpis GET /api/v1/frms/kpis read:frms
list_frms_models / listFrmsModels GET /api/v1/frms/models read:frms
predict_turnaround / predictTurnaround POST /api/v1/frms/predict write:frms
queue_turnaround_batch / queueTurnaroundBatch POST /api/v1/frms/predict/batch write:frms
record_allocation_decision / recordAllocationDecision POST /api/v1/frms/predictions/decide write:frms
verify_predictions / verifyPredictions POST /api/v1/frms/predictions/verify write:frms
get_active_rules / getActiveRules GET /api/v1/frms/rules/active/{airport} read:frms-rules
get_rule_effect / getRuleEffect GET /api/v1/frms/rules/effect read:frms-rules
list_local_rules / listLocalRules GET /api/v1/frms/rules/local read:frms-rules
set_local_rule / setLocalRule POST /api/v1/frms/rules/local write:frms-rules
list_rule_sets / listRuleSets GET /api/v1/frms/rules/sets read:frms-rules
get_schedule_config / getScheduleConfig GET /api/v1/frms/schedule/config/{airport} read:frms-schedule
set_schedule_config / setScheduleConfig POST /api/v1/frms/schedule/config/{airport} write:frms-schedule
preview_schedule / previewSchedule GET /api/v1/frms/schedule/preview/{airport} read:frms-schedule
list_frms_training_jobs / listFrmsTrainingJobs GET /api/v1/frms/train/jobs read:frms-training
queue_frms_training / queueFrmsTraining POST /api/v1/frms/train/queue write:frms-training
check_training_readiness / checkTrainingReadiness POST /api/v1/frms/training/readiness read:frms-training
get_airport_flights / getAirportFlights GET /api/v1/airports/{airportCode}/flights read:flights
get_flight_analytics / getFlightAnalytics GET /api/v1/flights/analytics read:flights
list_daily_flight_summaries / listDailyFlightSummaries GET /api/v1/flights/daily-summary read:flights
list_flight_history / listFlightHistory GET /api/v1/flights/history read:flights
list_live_flights / listLiveFlights GET /api/v1/flights/live read:flights
get_flight_statistics / getFlightStatistics GET /api/v1/flights/statistics read:flights
get_lidar_bottlenecks / getLidarBottlenecks GET /api/v1/lidar/bottlenecks read:lidar
get_lidar_heatmap / getLidarHeatmap GET /api/v1/lidar/heatmap read:lidar
get_lidar_historical / getLidarHistorical GET /api/v1/lidar/historical read:lidar
get_lidar_real_time / getLidarRealTime GET /api/v1/lidar/real-time read:lidar
list_maintenance_reports / listMaintenanceReports GET /api/v1/maintenance/reports read:maintenance
schedule_maintenance / scheduleMaintenance POST /api/v1/maintenance/schedule write:maintenance
list_custom_models / listCustomModels GET /api/v1/custom-models read:models
train_custom_model / trainCustomModel POST /api/v1/custom-models/train write:models
list_training_jobs / listTrainingJobs GET /api/v1/custom-models/training read:models
run_custom_model / runCustomModel POST /api/v1/custom-models/{modelId}/run write:models
list_models / listModels GET /api/v1/ml/models read:models
run_model_prediction / runModelPrediction POST /api/v1/models/{modelId}/predict write:models
predict_flight_delay / predictFlightDelay POST /api/v1/models/predict/delay read:predictions
predict_maintenance / predictMaintenance POST /api/v1/models/predict/maintenance read:predictions
get_current_resource_utilization / getCurrentResourceUtilization GET /api/v1/resource-optimization/resource-utilization read:resources
list_resource_history / listResourceHistory GET /api/v1/resources/history read:resources
list_safety_alerts / listSafetyAlerts GET /api/v1/safety/alerts read:safety
get_safety_analytics / getSafetyAnalytics GET /api/v1/safety/analytics read:safety
list_safety_history / listSafetyHistory GET /api/v1/safety/history read:safety
list_safety_metrics / listSafetyMetrics GET /api/v1/safety/metrics read:safety
list_weather_history / listWeatherHistory GET /api/v1/weather/history read:weather

Handling errors

Both clients raise typed errors, so you can catch the case you care about.

from sansai import (
    AuthenticationError,
    PermissionError_,
    InsufficientScopeError,
    QuotaExceededError,
    RateLimitError,
    NoDataError,
)

try:
    flights = sansai.live_flights(airport="LHR")
except AuthenticationError:
    print("The credential was not accepted.")
except InsufficientScopeError as e:
    print(f"Token is missing the {e.required_scope} scope.")
except PermissionError_:
    print("Your plan does not include this feature.")
except RateLimitError:
    print("Rate limited. The client already retried with backoff.")
except QuotaExceededError:
    print("Monthly plan allowance used. Retrying will not help until next month.")
except NoDataError:
    print("No stored records or trained model for this account.")
import {
  AuthenticationError,
  PermissionError,
  InsufficientScopeError,
  QuotaExceededError,
  RateLimitError,
  NoDataError,
} from "@kquika-inc/sansai";

try {
  const flights = await sansai.liveFlights({ airport: "LHR" });
} catch (e) {
  if (e instanceof AuthenticationError) console.error("Credential not accepted.");
  else if (e instanceof InsufficientScopeError) console.error(`Missing scope: ${e.requiredScope}`);
  else if (e instanceof QuotaExceededError) console.error("Monthly allowance used.");
  else if (e instanceof RateLimitError) console.error("Rate limited.");
  else throw e;
}

RateLimitError and QuotaExceededError are deliberately separate. The first clears within the window and is worth retrying. The second does not clear until the next calendar month, so a client that treats them alike will retry for weeks.

What the clients do for you

  • Auth. Set an API key or client credentials once; every request carries the right header. OAuth tokens are refreshed before expiry without a round trip from you.
  • Retries. A rate limit or a transient server failure is retried with exponential backoff and jitter, honoring Retry-After when it is sent. A rejected credential, an insufficient scope, or an exhausted monthly allowance is not retried, because retrying will not fix it.
  • Typed errors. AuthenticationError, PermissionError, InsufficientScopeError, RateLimitError, QuotaExceededError, NoDataError, NotFoundError, ServerError.
  • Flattened objects. Nested filter, meta, and prediction blocks are lifted onto the result, so delay_minutes and remaining_this_month are one attribute away.
  • Useful ordering. Live flights come back nearest first; safety alerts come back most severe first; bottlenecks come back worst first.
  • Named arguments, not payload dictionaries. Request bodies are flattened into keyword arguments, so a required field that is missing is a TypeError at the call site rather than a 400 from the server. Pass body= alongside them when you need to send a field the schema does not name yet.
  • Real types, not any. Every parameter, body field, and response is typed from the API specification. priority is 'low' | 'medium' | 'high' | 'critical', not string. flightHistory returns Result<Flight>, so delay_minutes completes in your editor and a typo fails tsc rather than production.

Artificial intelligence

Three of the endpoint groups are model-backed rather than lookups.

Delay prediction scores a departure against a model trained on your own movements, weather, and stand data. Maintenance prediction scores tracked systems for upcoming risk. Turnaround prediction scores resource needs per movement against the FRMS models. Custom models lets you train on your own dataset, poll the training job, and call the result through the same client.

job = sansai.train_model(dataset_id=42, target="delay_minutes")

for candidate in sansai.training_jobs():
    print(candidate.status, candidate.progress)

result = sansai.run_custom_model(job.model_id, features={"gate": "B22"})

The models learn your airport's patterns rather than an industry average, which is why a new account gets a 404 rather than a generic number. Nothing is inferred from another operator's data.

Predictions carry their own confidence, and the model that produced them is named in the response, so an output can always be traced back to a version:

prediction = sansai.predict_delay(scheduled_departure="2026-09-01T14:30:00Z")
print(prediction.delay_probability, prediction.confidence, prediction.model)

Every 200 from these endpoints is model output. Where no trained model exists you get NoDataError instead of an estimate, so a prediction in your system is never a placeholder.

Turnaround and resource allocation

The FRMS endpoints cover the allocation loop end to end: predict what a turnaround needs, record what the planner actually did, and verify the two against each other so the next training run learns from the difference.

predictions = sansai.predict_turnaround(body={"airport": "LHR", "date": "2026-09-01"})

sansai.record_decision(body={"prediction_id": 812, "accepted": True})

sansai.verify_predictions(body={"start_date": "2026-08-01", "end_date": "2026-08-29"})

Resource rules come in two layers, an imported set and local overrides, and the API reports what the combination actually permits:

print(sansai.active_rules("LHR"))
print(sansai.rule_effect())
sansai.set_local_rule(body={"resource": "wide_body_stand", "capability": "A380"})

Forward schedules, KPI targets, and FRMS training runs are reachable through the same client:

schedule = sansai.preview_schedule("LHR")
kpis = sansai.kpis()
job = sansai.queue_frms_training(body={"models": ["turnaround"], "airport": "LHR"})

Turnaround Allocation is licensed separately from the plan tiers, so holding the scopes is not enough on its own. Without the add-on you get 403 with code FRMS_ACCESS_REQUIRED. Talk to support@kquika.com about adding it.

They carry their own scopes: read:frms, write:frms, read:frms-rules, write:frms-rules, read:frms-schedule, write:frms-schedule, read:frms-training, and write:frms-training.

Reading the fields

Field Meaning
delay_minutes Minutes late. Negative means early.
delay_probability 0 to 100, the modeled chance of a delay.
predicted_delay_minutes Expected delay if one occurs.
confidence 0 to 100, how much weight the model puts on its own answer.
utilization_percentage 0 to 100, how hard a stand or resource is working.
efficiency_change Percentage points against the previous period.
severity advisory, warning, or critical.
visibility Statute miles.
wind_speed Knots.
altitude Feet above mean sea level.
generated_at RFC 3339 UTC, ending in Z. Use this, not any timestamp field.

Limits and quotas

Three separate ceilings apply. Your position against all three comes back on every call, so you never have to discover a limit by hitting it.

result = sansai.flight_history(airport="LHR")
print(result.meta.rate_limit.remaining_today)
print(result.meta.subscription.plan, result.meta.subscription.remaining_this_month)
Limit Scope Resets
Per-minute and per-day rate limit One credential Within the window
Monthly call allowance Your account, across every credential it owns Start of the calendar month
Minimum plan tier Optional, set per credential On upgrade

Monthly allowance by plan: Standard 10,000, Professional 50,000, Enterprise 75,000, Custom negotiated. See https://www.sans-ai.tech/pricing.

List endpoints accept limit and do not paginate. Values above each ceiling are clamped, so split larger windows with start_date and end_date rather than trying to page.

Data guarantees

Every successful response carries observed records or trained model output. The API never substitutes estimated, sample, or heuristic values. Where no records or no trained model exist you get NoDataError, so absence is never mistaken for a measurement.

If you are feeding this into a system that assigns stands or dispatches crew, that guarantee is the point. You do not need a defensive check on every field to ask whether the number is real.

Need a client in another language?

The SDKs are generated from an OpenAPI specification, which the API serves directly, so the contract you generate from is always the one the service is running:

curl -O https://www.sans-ai.tech/api/openapi.yaml     # as authored
curl -O https://www.sans-ai.tech/api/openapi.json     # for tooling that wants JSON

Both are unauthenticated, because the spec describes the shape of the API rather than any data. Every endpoint it documents still needs a credential.

Point your generator of choice at it, for example:

openapi-generator-cli generate \
  -i https://www.sans-ai.tech/api/openapi.yaml \
  -g go -o ./sansai-go

Citation

If SANS.AI informs a paper, technical report, operations study, or regulatory submission, cite the release you actually ran.

Two things here are citable and they are not the same. Cite SANS.AI when your results depend on its data or predictions, which is the usual case. Cite the client library only when the work is about the client itself, or when a reviewer needs to reproduce the exact calls you made.

Citing SANS.AI

The API documentation renders the current platform citation in BibTeX, APA, IEEE, and CITATION.cff, generated from the running service so the version is never stale: https://www.sans-ai.tech/documentation

That page is the source to copy from. This README deliberately does not restate the platform version, because a version pinned in a package README goes out of date the moment the service ships a release.

Citing this client

BibTeX

@misc{kquika2026sansaipy,
  title        = {SANS.AI Platform Python SDK},
  author       = {{Kquika, Inc.}},
  year         = {2026},
  version      = {1.0.0},
  publisher    = {{Kquika, Inc.}},
  howpublished = {\url{https://pypi.org/project/kquika-sansai/}},
  url          = {https://pypi.org/project/kquika-sansai/},
  note         = {Computer software, version 1.0.0}
}

@misc{kquika2026sansaijs,
  title        = {SANS.AI Platform Node SDK},
  author       = {{Kquika, Inc.}},
  year         = {2026},
  version      = {1.0.0},
  publisher    = {{Kquika, Inc.}},
  howpublished = {\url{https://www.npmjs.com/package/@kquika-inc/sansai}},
  url          = {https://www.npmjs.com/package/@kquika-inc/sansai},
  note         = {Computer software, version 1.0.0}
}

@misc is used because every BibTeX style accepts it. On biblatex you can change the type to @software and drop howpublished.

APA 7th

Kquika, Inc. (2026). SANS.AI Platform Python SDK (Version 1.0.0) [Computer software]. https://pypi.org/project/kquika-sansai/

Kquika, Inc. (2026). SANS.AI Platform Node SDK (Version 1.0.0) [Computer software]. https://www.npmjs.com/package/@kquika-inc/sansai

In text: (Kquika, Inc., 2026).

IEEE

Kquika, Inc., "SANS.AI Platform Python SDK," version 1.0.0, 2026. [Online]. Available: https://pypi.org/project/kquika-sansai/

Kquika, Inc., "SANS.AI Platform Node SDK," version 1.0.0, 2026. [Online]. Available: https://www.npmjs.com/package/@kquika-inc/sansai

CITATION.cff

Drop this in the root of a repository that depends on SANS.AI and reference managers will pick it up. Keep the entry for the client you actually use.

cff-version: 1.2.1
message: "If you use SANS.AI in your work, please cite it as below."
title: "SANS.AI Platform Python SDK"
type: software
version: "1.0.0"
license: MIT
url: "https://pypi.org/project/kquika-sansai/"
repository-artifact: "https://pypi.org/project/kquika-sansai/"
authors:
  - name: "Kquika, Inc."

For the Node client, change title to SANS.AI Platform Node SDK and both URLs to https://www.npmjs.com/package/@kquika-inc/sansai.

Reporting the version

Pin the version in your methods section. The client reports itself in the User-Agent on every request, so a server log line and a paper can be tied together:

sansai-python/1.0.1
sansai-node/1.0.1

Read it at run time rather than transcribing it, so the number in your write up cannot drift from the number that ran:

import sansai
print(sansai.__version__)
import { VERSION } from "@kquika-inc/sansai";
console.log(VERSION);

Predictions also depend on which models produced them, and those version independently of both the API and this client. Call sansai.models() at run time and record the model version alongside the API version, so a reviewer can tell which models produced your numbers.

Support

API reference: https://www.sans-ai.tech/documentation

Questions, a key, or a raised limit: support@kquika.com

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kquika_sansai-1.0.1.tar.gz (15.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kquika_sansai-1.0.1-py3-none-any.whl (17.6 kB view details)

Uploaded Python 3

File details

Details for the file kquika_sansai-1.0.1.tar.gz.

File metadata

  • Download URL: kquika_sansai-1.0.1.tar.gz
  • Upload date:
  • Size: 15.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for kquika_sansai-1.0.1.tar.gz
Algorithm Hash digest
SHA256 26128ac1f5e77ecdd58bc9784842b7632a3c60cc452011053e3a51607afda8b6
MD5 9f9ddf82eba6faa0c9a867398998dbc5
BLAKE2b-256 acbee68c8d35a57945ad927e9a2f5b7b78d4fbd5789bc51bd690298e093b08f1

See more details on using hashes here.

File details

Details for the file kquika_sansai-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: kquika_sansai-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 17.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.7

File hashes

Hashes for kquika_sansai-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0f84f2cb3e9c634462626f59d184dd236a0b18cd851179613725c562022a7f77
MD5 bfc9f6e32a085a1b3a8d97f320221f2b
BLAKE2b-256 1fd22d42749346d8c6941ea973e5bd0eaa645246ba2361c6806d93f08f84239b

See more details on using hashes here.

Release history Release notifications | RSS feed

1.0.2

2 files

This release

1.0.1 This release

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page