Bulgan Trip

Хөгжүүлэгчийн заавар

Булган аймгийн үйлчилгээний газруудын мэдээллийг нээлттэй API-аас авч, өөрийн аппдаа харуулах, дарахад Bulgan Trip аппыг нээх заавар.

1. Quick start

Authentication шаардлагагүйAPI key заавал биш Бүх endpoint нээлттэй, HTTPS. Base URL:

https://api.bulgan-trip.mn/api/v1

Гурван request-ээр бүх зүйл гарна:

# 1. Аймгийн id авна (одоогоор зөвхөн Булган)
curl -s "https://api.bulgan-trip.mn/api/v1/provinces"

# 2. Тэр id-гаар бүх өгөгдлийг нэг дор татна
curl -s "https://api.bulgan-trip.mn/api/v1/sync/snapshot?province=<province_id>&locale=mn"

# 3. Дараа нь өөрчлөгдсөн эсэхийг л шалгана (хөнгөн)
curl -s "https://api.bulgan-trip.mn/api/v1/sync/manifest?province=<province_id>"

2. Endpoints

Province — GET /provinces

[
  {
    "id": "4e3af807-59c2-40dc-8bcc-7d613f2bacdd",
    "code": "bulgan",
    "name_mn": "Булган",
    "name_en": "Bulgan",
    "status": "active"
  }
]

Одоогоор зөвхөн Булган идэвхтэй. Энэ id-г доорх request-үүдэд хэрэглэнэ.

Snapshot — GET /sync/snapshot?province=<id>&locale=mn

Жагсаалт барихад энэ нэг request хангалттай.

Ойролцоогоор 500 КБ. Бүх идэвхтэй place, category, district, route, event-ийг агуулна. Хариу:

{
  "synced_at":   "2026-09-29T11:57:38Z",
  "province_id": "4e3af807-…",
  "categories":  [ … 15 ],
  "districts":   [ … 16 ],
  "places":      [ … 163 ],
  "routes":      [ … ],
  "events":      [ … ]
}

places дотор зөвхөн нийтлэгдсэн, идэвхтэй газрууд ирнэ. Нуусан эсвэл устгасан газар огт орохгүй тул тусад нь шүүх шаардлагагүй.

Manifest — GET /sync/manifest?province=<id>

{
  "province_id": "4e3af807-…",
  "updated_at":  "2026-09-29T09:14:45Z",
  "counts": { "places": 163, "categories": 15, "routes": 2, "events": 3 }
}

Хэдхэн байт. updated_at өөрчлөгдсөн үед л snapshot-оо дахин татна. Өдөрт нэг удаа poll хийхэд хангалттай.

Filter, search — GET /places

Server талаас filter, search, pagination хийх шаардлагатай бол:

GET /places?locale=mn&page=1&page_size=50
GET /places?locale=mn&q=рашаан
GET /places?locale=mn&category=<category_id>
GET /places?locale=mn&lat=48.8125&lng=103.5347          # зайгаар эрэмбэлнэ
GET /places?locale=mn&lat=…&lng=…&radius_km=10          # ойролцоох

Response нь { "items": [...], "page": 1, "page_size": 50 }. lat/lng өгвөл газар бүрт distance_m нэмэгдэнэ — энэ нь замаар явах бодит зай (шулуун шугамын зай биш), метрээр. page parameter-ийг үргэлж тодорхой бичиж өгнө үү.

3. Place object

Хамгийн их хэрэглэгддэг field-үүд:

FieldТайлбар
idUUID. Хэзээ ч өөрчлөгдөхгүй. Өөрийн талдаа энэ id-гаар холбоно.
deeplinkАппыг нээх URL. Доорх 4-р хэсгийг үзнэ үү.
translationsmn, en, ko, zh, ru key-тэй. Тус бүрд name, short_desc, description.
location{ "lat": 49.169, "lng": 102.351 }
cover_image_urlҮндсэн зургийн бүтэн URL. Байхгүй бол null.
imagesБүх зураг: { id, url, is_cover, sort_order }
category_idSnapshot-ийн categories-той тааруулна.
district_idСум. districts-тай тааруулна.
contacts[{ "type": "phone", "value": "95826063" }] — дугаар бүр тусдаа мөр.
price_statusfree | paid | unknown
price_min, price_maxҮнийн хязгаар. price_currency ихэвчлэн MNT. Байхгүй бол null.
is_24h, open_time, close_timeАжиллах цаг. Цаг нь "09:00" хэлбэртэй, Улаанбаатарын цагаар.
slugCache хийж хадгалж болохгүй. Админаас гараар засагддаг тул хожим өөрчлөгдөж болно. Identifier болгож зөвхөн id хэрэглэнэ.

Place бүрийн deeplink field-ийг хэвээр нь хэрэглэнэ. Өөрөө URL угсрах шаардлагагүй.

"deeplink": "https://bulgan-trip.mn/place/4a569305-c799-4529-9d9d-534e6c9e44d5"

Энэ бол энгийн HTTPS URL. Хэрэглэгч дээр нь дарахад:

Сонгох цонх гарахгүй, «апп олдсонгүй» алдаа гарахгүй. Та апп суусан эсэхийг шалгах шаардлагагүй — үүнийг үйлдлийн систем өөрөө хийнэ.

Код жишээ

// Android (Kotlin)
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(place.deeplink)))
// iOS (Swift)
if let url = URL(string: place.deeplink) {
    UIApplication.shared.open(url)
}
// Flutter
import 'package:url_launcher/url_launcher.dart';
await launchUrl(Uri.parse(place.deeplink), mode: LaunchMode.externalApplication);
<!-- Вэб -->
<a href="https://bulgan-trip.mn/place/4a569305-…">Дэлгэрэнгүй</a>

Анхаарах: deeplink-ийг WebView дотор биш, external байдлаар нээнэ үү (Flutter дээр LaunchMode.externalApplication). WebView дотор нээвэл апп руу шилжихгүй, зүгээр вэб хуудас болж харагдана.

Мөн bulgan-trip:// гэсэн схем байдаг ч түүнийг хэрэглэхгүй: апп суулгаагүй үед чимээгүй бүтэлгүйтдэг тул fallback байхгүй.

5. Locale, зураг

Locale. Snapshot-д 5 хэлний орчуулга бүгд ирдэг тул хэл солиход дахин татах шаардлагагүй — translations дотроос сонгоод харуулна. Тухайн locale-д орчуулга байхгүй бол mn-г fallback болгож хэрэглэнэ.

Зураг. Бүх зураг https://cdn.bulgan-trip.mn/… дээр байрлана, шууд ачаалж болно. Resize хийх query parameter байхгүй тул өөрийн талдаа cache хийж, багасгахыг зөвлөе. Зураггүй газар байж болох тул cover_image_url нь null байх тохиолдлыг заавал бодолцоорой.

6. Rate limit, API key

ДүрэмЯагаад
GET /places/:id-г poll хийж бүү дуудRequest бүр тухайн газрын view count-ыг нэмдэг тул манай статистик гуйвна. Жагсаалтаа snapshot-оос ав.
Өгөгдлийг өөртөө cache хийContent өдөр бүр өөрчлөгддөггүй. manifest-ээр шалгаад шаардлагатай үед л snapshot-оо дахин тат.
Identifier болгож id хэрэглэslug өөрчлөгдөж болно, id өөрчлөгдөхгүй.
deeplink-ийг хэвээр нь хадгалФорматыг бид цаашид өөрчилж болно. Field-ийг нь хэрэглэвэл танд өөрчлөлт хэрэггүй.
AttributionМэдээлэл Bulgan Trip-ээс гаралтай гэдгийг хэрэглэгчдэд ойлгомжтой байлгана уу.

Rate limit. IP хаяг тус бүрээр:

EndpointRate limit
/sync/snapshot, /routing/*минутад 30
бусад бүх нээлттэй endpointминутад 240

Хэвийн ашиглалтад хүрэхгүй тоо. Хэтэрвэл 429 Too Many Requests буцаах бөгөөд Retry-After header-т хэдэн секунд хүлээхийг бичнэ. Snapshot-оо нэг удаа татаад cache хийвэл энэ limit-д огт ойртохгүй.

API key (санал болгоё)

Бидэнтэй холбогдож API key авч болно. API key-тэй бол:

EndpointAPI key-гүйAPI key-тэй
/sync/snapshot, /routing/*минутад 30минутад 120
бусадминутад 240минутад 600

Request бүрдээ ийн бичнэ:

X-API-Key: bt_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

API key-гүй ч ажиллана. Энэ нь authentication биш — зөвхөн таныг таних, илүү өндөр rate limit өгөх зориулалттай. Буруу эсвэл хүчингүй болсон key илгээсэн ч request татгалзагдахгүй, зүгээр key-гүйтэй адил limit-тэй болно.

Key-г нууцлах шаардлагагүй ч бусадтай хуваалцахгүй байхыг хүсье — асуудал гарвал бид тухайн key-г л идэвхгүй болгоно.

Нэмэлт. Request бүрдээ X-Client header-т аппынхаа нэр, хувилбарыг бичиж өгвөл баярлалаа (жишээ нь X-Client: tavan-bogd-app/2.1).

CORS. Browser-оос шууд fetch хийх бол таны origin-ийг манай allowlist-д нэмэх шаардлагатай — бидэнд domain-аа илгээнэ үү. Native app эсвэл server-side request-д CORS хамаарахгүй.

7. Холбоо барих

Асуулт, CORS-д domain нэмүүлэх, bug мэдээлэх: info@bulgan-trip.mn

Апп: bulgan-trip.mn · Ашиглалтын заавар