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_entitlements / getEntitlements |
GET /api/v1/entitlements |
read:account |
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 |
acknowledge_safety_alert / acknowledgeSafetyAlert |
POST /api/v1/safety/alerts/{alertId}/acknowledge |
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-Afterwhen 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_minutesandremaining_this_monthare 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
TypeErrorat the call site rather than a 400 from the server. Passbody=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.priorityis'low' | 'medium' | 'high' | 'critical', notstring.flightHistoryreturnsResult<Flight>, sodelay_minutescompletes in your editor and a typo failstscrather 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.
Safety alerts and their evidence
Every alert carries how it came about and what was decided about it. Provenance is set when the alert is created and never edited.
alerts = sansai.safety_alerts(severity="critical")
for alert in alerts:
print(alert.provenance.detection_method, alert.provenance.model_version)
print(alert.disposition.state, alert.disposition.acknowledged_at)
sansai.acknowledge_alert(
alerts[0].id,
disposition="false_positive",
note="Vehicle had clearance, radio log confirms",
)
Dispositions are actioned, stood_down, false_positive, duplicate and
referred. The last three resolve the alert; the first two leave it open.
Alerts with detection_method of sample are demonstration content shown to
accounts with no safety data yet. They are never persisted and cannot be
dispositioned.
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. |
Knowing what you can reach
Rather than discovering an entitlement by receiving a 403, ask:
access = sansai.entitlements()
print(access.plan.name)
print([f.name for f in access.features])
for add_on in access.add_ons:
print(add_on.label, "enabled" if add_on.enabled else "not licensed")
The response reports the plan behind the credential, the features on the account, which add-ons are licensed, and the scopes the credential carries. An integration can check once at start-up and route around anything it cannot use.
Knowing when a list is cut short
List endpoints clamp above a ceiling. Every list response now says whether it did:
result = sansai.flight_history(airport="LHR", limit=5000)
print(result.meta.page.limit) # 2000, the applied ceiling
print(result.meta.page.limit_requested) # 5000, what you asked for
print(result.meta.page.truncated) # True, more records may exist
truncated is true when the response filled the applied limit, so more records
may sit beyond it. Narrow the date range and call again.
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.2, 2026. [Online]. Available: https://pypi.org/project/kquika-sansai/
Kquika, Inc., "SANS.AI Platform Node SDK," version 1.0.2, 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.2
sansai-node/1.0.2
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file kquika_sansai-1.0.2.tar.gz.
File metadata
- Download URL: kquika_sansai-1.0.2.tar.gz
- Upload date:
- Size: 16.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
04ee84c7d37a93588002be2ed2d0b457f2b8a8a0e4e05325ac9780cacb8a0ff3
|
|
| MD5 |
0fcd2dd05503f1d79104dd93cfb56ee6
|
|
| BLAKE2b-256 |
09d58e1cebb093b4da9d6846565a7e3b9acd2eefd56fd9b3af0b5f8161abe9f6
|
File details
Details for the file kquika_sansai-1.0.2-py3-none-any.whl.
File metadata
- Download URL: kquika_sansai-1.0.2-py3-none-any.whl
- Upload date:
- Size: 18.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3652378c479834d3b14306e1018d2de1c458be1ea0482fa94722952ecdfa5eb8
|
|
| MD5 |
df4d169ad041ef3a5e59195045dcdd63
|
|
| BLAKE2b-256 |
171e86d9caad619cdbbe48ab8550bdb2bb3e3aaf97112fefd544274622a8236b
|