Fe Alsika — API

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/stations for 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, send locale as a query string parameter (e.g. ?locale=ar). The request body is ignored on GET. Optionally set Accept-Language: ar (or en) as a fallback when locale is omitted; the default is ar. Authorization stays in the header only (Bearer token).
  • Identifiers: metro/LRT stations use metro_stations.id; train cities are referenced via one train_stops.id per city (facet=stop_ids); public transport uses stable numeric place_ids from GET …/public-transport/places.

Route map

PurposeVerbPath
Metro/LRT paginated station listGET/metro/stations
Metro/LRT station detailGET/metro/stations/{id}
Metro/LRT lines + stations (graph)POST/metro/lines/stations
Metro/LRT route planGET/metro/routes
Metro/LRT name suggestionsGET/metro/search/suggestions
Train route searchGET/trains/search
Train city suggestionsGET/trains/search/suggestions
Train by IDGET/trains/by-id
Train segmentGET/trains/routes/by-segment
Trains between stopsGET/trains/schedules/between-stops
Train metadataGET/trains/metadata
Public transport place listGET/public-transport/places
Public transport routeGET/public-transport/routes
Public transport route searchGET/public-transport/search
Public transport suggestionsGET/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

HTTPcodeMeaning
401missing_api_keyEmpty or missing Authorization: Bearer
401invalid_api_keyToken not recognized
401request_blockedPayload failed security scan
402insufficient_balanceWallet too low for this category (see data)
400validation_errorMissing or invalid parameters
400route_errorMetro planner could not build a route
200not_foundDomain "not found" (train / public transport) — check success
429rate_limit_exceededToo 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 & pathBilling categoryPrimary data shape
GET /metro/stationsmetroArray of station rows + meta.pagination
GET /metro/stations/{id}metroSingle station (detail)
POST /metro/lines/stationsmetro{ lines: [...] } full graph
GET /metro/routesmetroRoute 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/suggestionsmetroMetro/LRT station name suggestions
GET /trains/searchsearchArray of type: train route results
GET /trains/search/suggestionssearch_suggestionsTrain city suggestions
GET /public-transport/placespublic_transportPlace list / graph
GET /public-transport/routespublic_transportRoute between place ids
GET /public-transport/searchpublic_transportArray of type: public_transport results
GET /public-transport/search/suggestionspublic_transportPublic transport place suggestions
GET /trains/by-idtrain_lookupTrain object with stations
GET /trains/routes/by-segmenttrain_segmentTrain object for segment
GET /trains/schedules/between-stopstrain_betweenArray of train summaries
GET /trains/metadatatrain_referenceArray 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/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/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 } }
}