HTTP API reference
Base: https://fealsika.goosek.com/api/v1
· Authorization: Bearer <API_KEY>
· Accept: application/json
· Reads use GET (query string). Only POST https://fealsika.goosek.com/api/v1/metro/lines/stations uses a JSON body (full metro/LRT graph).
Overview
Transit APIs: metro/LRT (including search suggestions), national rail, and local public transport (separate from metro). Each domain has its own URL prefix. All v1 routes require a valid developer API key (or configured legacy token).
Production. Use HTTPS only. Treat API keys like passwords; never embed them in client-side source or public repos.
HTTP verbs
Each path is registered once with a single verb. No duplicate aliases.
- GET — all searches, lists, lookups, plans, and train helpers (parameters in the query string).
- POST — only
/metro/lines/stationsfor the bulk lines+stations graph (Content-Type: application/json).
Naming & URL conventions
- Canonical paths use kebab-case segments (e.g.
public-transport/routes,trains/routes/by-segment). - No alternate/alias paths. Each endpoint has exactly one URL.
- Parameters use
snake_case. - Locale (
ar|en) — for GET endpoints, sendlocaleas a query string parameter (e.g.?locale=ar). The request body is ignored on GET. Optionally setAccept-Language: ar(oren) as a fallback whenlocaleis omitted; the default isar.Authorizationstays in the header only (Bearer token). - Identifiers: metro/LRT stations use
metro_stations.id; train cities are referenced via onetrain_stops.idper city (facet=stop_ids); public transport uses stable numericplace_ids fromGET …/public-transport/places.
Route map
| Purpose | Verb | Path |
|---|---|---|
| Metro/LRT paginated station list | GET | /metro/stations |
| Metro/LRT station detail | GET | /metro/stations/{id} |
| Metro/LRT lines + stations (graph) | POST | /metro/lines/stations |
| Metro/LRT route plan | GET | /metro/routes |
| Metro/LRT name suggestions | GET | /metro/search/suggestions |
| Train route search | GET | /trains/search |
| Train city suggestions | GET | /trains/search/suggestions |
| Train by ID | GET | /trains/by-id |
| Train segment | GET | /trains/routes/by-segment |
| Trains between stops | GET | /trains/schedules/between-stops |
| Train metadata | GET | /trains/metadata |
| Public transport place list | GET | /public-transport/places |
| Public transport route | GET | /public-transport/routes |
| Public transport route search | GET | /public-transport/search |
| Public transport suggestions | GET | /public-transport/search/suggestions |
Authentication
Send the secret token in the Authorization header (OAuth2-style bearer, not a query parameter).
Authorization: Bearer YOUR_API_KEY
Create and rotate keys in the developer portal: /developer/api-keys. Wallet billing applies per successful request unless a legacy operator token is used (see API_LEGACY_BEARER_TOKEN in server config).
Security model
Transport & keys
- Use TLS in production; do not send keys over plain HTTP.
- Store keys in secure storage (OS keychain, server secrets), not in git or mobile app binaries when avoidable.
- Rotate keys if leaked; revoke old keys from the portal.
Rate limiting
All /api/v1/* routes are throttled per bearer token (SHA-256 hash of the token as the limiter key) or per client IP when no token is sent. Default: API_RATE_LIMIT_PER_MINUTE (see server .env). Exceeded requests return 429 Too Many Requests with standard Laravel JSON.
Request inspection
Query strings and the POST JSON body (metro graph only) are scanned (including nested arrays in JSON) for common injection patterns (SQL fragments, script tags, etc.). Suspicious payloads are rejected with 401 and code: "request_blocked". Very strict substrings (e.g. ; or -- in free text) may false-positive; prefer passing numeric ids instead of raw names where the API allows it.
Logging
API access is logged (path, method, status, duration, billing category, optional charge). Blocked attack attempts log a short sample of the offending string, not the full body.
Documentation site (this page)
HTML responses include security headers: X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin, restrictive Permissions-Policy, and Content-Security-Policy: frame-ancestors 'self'. A stricter full-page CSP can be added at the reverse proxy if you move styles to external files.
Response format (JSON envelope)
Application handlers return a consistent envelope. Always check success and HTTP status together.
Success (2xx)
{
"success": true,
"message": "Human-readable summary.",
"data": { },
"meta": {
"pagination": {
"current_page": 1,
"per_page": 10,
"total": 42,
"last_page": 5
}
}
}
meta is omitted when empty. Paginated list endpoints put the page slice in data (array) and pagination in meta.pagination.
Pagination limits
Every endpoint that accepts per_page or legacy count_per_page clamps the effective page size to at most 30. Larger values are reduced to 30. Defaults per endpoint are also capped (e.g. a documented default of 50 becomes 30 unless you pass a smaller per_page). Server setting: API_MAX_PER_PAGE in .env (still capped at 30 in application config).
Failure (4xx / 5xx from handlers)
{
"success": false,
"message": "What went wrong.",
"data": null,
"code": "validation_error"
}
data may hold structured hints (e.g. billing breakdown for 402).
HTTP status & code values
| HTTP | code | Meaning |
|---|---|---|
| 401 | missing_api_key | Empty or missing Authorization: Bearer |
| 401 | invalid_api_key | Token not recognized |
| 401 | request_blocked | Payload failed security scan |
| 402 | insufficient_balance | Wallet too low for this category (see data) |
| 400 | validation_error | Missing or invalid parameters |
| 400 | route_error | Metro planner could not build a route |
| 200 | not_found | Domain "not found" (train / public transport) — check success |
| 429 | rate_limit_exceeded | Too many requests (see Retry-After header) |
Billing
Successful responses (2xx with success: true) may debit the developer's prepaid balance by category (metro, search, search_suggestions, public_transport, train helpers, etc.). Prices are configured under /admin/api-pricing. Insufficient balance returns 402 before the controller runs. Legacy bearer token skips per-user charging but requests are still logged.
Endpoint catalog
| Verb & path | Billing category | Primary data shape |
|---|---|---|
GET /metro/stations | metro | Array of station rows + meta.pagination |
GET /metro/stations/{id} | metro | Single station (detail) |
POST /metro/lines/stations | metro | { lines: [...] } full graph |
GET /metro/routes | metro | Route object with firstLineStations/secondLineStations (no per-station coordinates), transfers, ticket_price, duration_minutes, metro_service_window, journey_time (app-style), maps block with endpoint coordinates + Google Maps URL |
GET /metro/search/suggestions | metro | Metro/LRT station name suggestions |
GET /trains/search | search | Array of type: train route results |
GET /trains/search/suggestions | search_suggestions | Train city suggestions |
GET /public-transport/places | public_transport | Place list / graph |
GET /public-transport/routes | public_transport | Route between place ids |
GET /public-transport/search | public_transport | Array of type: public_transport results |
GET /public-transport/search/suggestions | public_transport | Public transport place suggestions |
GET /trains/by-id | train_lookup | Train object with stations |
GET /trains/routes/by-segment | train_segment | Train object for segment |
GET /trains/schedules/between-stops | train_between | Array of train summaries |
GET /trains/metadata | train_reference | Array of ids, stop ids, names, or class strings (facet-dependent) |
Below, each domain has its own section with compact example request (query or JSON body) and example response shapes.
Metro and LRT
Prefix https://fealsika.goosek.com/api/v1/metro/…. Billing category metro unless noted. Examples show the JSON query (GET) or body (POST) and a simplified success envelope; real rows include more fields. Per-station latitude/longitude and coordinates on each station are not returned in lists, line graphs, or route leg arrays; route maps includes origin/destination endpoint coordinates and a ready Google Maps URL. The server uses full coordinates internally for distance, pricing, and routing.
GET /metro/stations
Paginated stations. At least one filter: line selector, network_type, q, or is_interchange.
Example request (query)
{
"network_type": "metro",
"q": "attaba",
"locale": "ar",
"page": 1,
"per_page": 20
}
Example response
{
"success": true,
"message": "…",
"data": [
{
"id": 12,
"metro_line_id": 1,
"stop_order": 5,
"name_ar": "العتبة",
"name_en": "Attaba",
"is_full_show": true,
"line": { "id": 1, "line_number": "2", "network_type": "metro" }
}
],
"meta": {
"pagination": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 }
}
}
Also supported: include_connections=1, metro_line_id, line_ids[], line_number, line_sort_order + network_type.
GET /metro/stations/{id}
Single station; {id} = metro_stations.id.
Example request (query)
{
"path": { "id": 12 },
"query": { "locale": "ar" }
}
Example response
{
"success": true,
"message": "…",
"data": {
"id": 12,
"name_ar": "…",
"name_en": "Attaba",
"connections": [],
"tour_guide": null,
"line": { "interchanges": [] }
}
}
POST /metro/lines/stations
Full metro/LRT graph. Content-Type: application/json.
Example request (JSON body)
{
"locale": "ar",
"include_lrt": true,
"lines": ["1", "2"]
}
Example response
{
"success": true,
"message": "…",
"data": {
"lines": [
{
"network": "metro",
"line_number": "1",
"name": "…",
"color": "#ee2e24",
"stations": [
{ "id": 1, "name": "…", "connections": [], "isFullShow": true }
],
"interchange": []
}
]
}
}
GET /metro/routes
Same network for origin and destination. Text + planner names follow locale.
Example request (query)
{
"origin_station_id": 1,
"destination_station_id": 40,
"locale": "ar"
}
Example response (data excerpt)
{
"success": true,
"message": "Metro route planned.",
"data": {
"locale": "ar",
"line": "1",
"number": "1",
"station_count": 8,
"ticket_price": 10,
"ticket_currency": "EGP",
"duration_minutes": 16,
"distance": 8.2,
"routeDescription": "…",
"firstLineStations": [{ "id": 1, "name": "…", "connections": [], "isFullShow": true }],
"secondLineStations": [],
"interchangeStations": [],
"transfers": [],
"metro_service_window": {
"timezone": "Africa/Cairo",
"is_open_now": true,
"message_ar": "…",
"message_en": "…"
},
"journey_time": { "total_minutes": 14, "formatted_hh_mm": "00:14", "label_ar": "…" },
"maps": {
"travel_mode": "transit",
"origin_coordinates": { "latitude": 30.05, "longitude": 31.24 },
"destination_coordinates": { "latitude": 30.07, "longitude": 31.22 },
"station_to_station_google_maps_url": "https://www.google.com/maps/dir/?api=1&origin=…&destination=…&travelmode=transit",
"template_url": "https://www.google.com/maps/dir/?api=1&origin={origin_latlng}&destination={destination_latlng}&travelmode=transit",
"placeholder_origin": "{origin_latlng}",
"placeholder_destination": "{destination_latlng}",
"hint_en": "…",
"hint_ar": "…"
}
}
}
GET /metro/search/suggestions
Metro/LRT station name autocomplete.
Example request (query)
{
"query": "sad",
"locale": "ar",
"page": 1,
"per_page": 20
}
Example response
{
"success": true,
"message": "…",
"data": [
{ "type": "metro_station", "id": 3, "name": "السادات" }
],
"meta": { "suggestion_pool_size": 2, "pagination": { "current_page": 1, "per_page": 20, "total": 2, "last_page": 1 } }
}
Trains
Prefix https://fealsika.goosek.com/api/v1/trains/…. Search uses billing search / search_suggestions; other train endpoints use their own categories (see catalog).
GET /trains/search
Coordinates and/or stop ids (see table in catalog for combinations).
Example request (query)
{
"origin": "30.05,31.24",
"destination": "31.2,29.9",
"page": 1,
"per_page": 10,
"locale": "ar"
}
Example response
{
"success": true,
"message": "…",
"data": [
{
"type": "train",
"train_id": 101,
"stations": []
}
],
"meta": { "pagination": { "current_page": 1, "per_page": 10, "total": 3, "last_page": 1 } }
}
GET /trains/search/suggestions
Example request (query)
{ "query": "اسكندر", "page": 1, "per_page": 15 }
Example response
{
"success": true,
"data": [
{ "type": "train_city", "id": 5001, "name": "الإسكندرية" }
],
"meta": { "pagination": { "current_page": 1, "per_page": 15, "total": 1, "last_page": 1 } }
}
GET /trains/by-id
Example request (query)
{ "train_id": 101 }
Example response
{
"success": true,
"data": {
"train_id": 101,
"stations": [{ "stop_id": 1, "city": "…", "departure_time": "08:00" }]
}
}
Unknown id: "success": false, "code": "not_found".
GET /trains/routes/by-segment
Example request (query)
{
"train_id": 101,
"origin_stop_id": 10,
"destination_stop_id": 25
}
Example response
{ "success": true, "data": { "train_id": 101, "stations": [] } }
GET /trains/schedules/between-stops
Example request (query)
{
"origin_stop_id": 10,
"destination_stop_id": 25,
"train_type": "express",
"page": 1,
"per_page": 20
}
Example response
{
"success": true,
"data": [
{ "train_id": 101, "departure": "09:15", "arrival": "12:40" }
],
"meta": { "pagination": { "current_page": 1, "per_page": 20, "total": 5, "last_page": 1 } }
}
GET /trains/metadata
facet required: ids | stop_ids | names | class.
Example request (query)
{ "facet": "stop_ids", "page": 1, "per_page": 30 }
Example response
{
"success": true,
"data": [5001, 5002, 5003],
"meta": { "facet": "stop_ids", "pagination": { "current_page": 1, "per_page": 30, "total": 120, "last_page": 4 } }
}
Public transport
Prefix https://fealsika.goosek.com/api/v1/public-transport/…. Billing public_transport. Place ids come from GET …/places.
GET /public-transport/places
Example request (query)
{ "page": 1, "per_page": 20, "include_station_graph": true }
Example response
{
"success": true,
"data": [
{ "id": 1, "name": "Place A", "connection_ids": [2, 3], "connections": ["Place B", "Place C"] }
],
"meta": { "format": "station_graph", "pagination": { "current_page": 1, "per_page": 20, "total": 40, "last_page": 2 } }
}
GET /public-transport/routes
Example request (query)
{ "origin_place_id": 1, "destination_place_id": 8 }
Example response
{
"success": true,
"data": {
"from": "Place A",
"to": "Place B",
"steps": [],
"time": "25 min",
"distance": 4.2
}
}
GET /public-transport/search
Example request (query)
{
"origin": "30.0,31.2",
"destination_place_id": 5,
"page": 1,
"per_page": 10
}
Example response
{
"success": true,
"data": [
{ "type": "public_transport", "from": "…", "to": "…", "time": "20 min" }
],
"meta": { "pagination": { "current_page": 1, "per_page": 10, "total": 2, "last_page": 1 } }
}
GET /public-transport/search/suggestions
Example request (query)
{ "query": "ram", "page": 1, "per_page": 20 }
Example response
{
"success": true,
"data": [
{ "type": "public_transport_place", "id": 3, "name": "Ramses" }
],
"meta": { "pagination": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 } }
}