# Index Snapshot API — Integration Reference
This document describes the `/api/index-snapshot` endpoint served by the **liquidityRisk** (Laravel) backend. It is written for the external/consuming server so its agent can debug integration issues.
---
## Endpoint
```
GET /api/index-snapshot
```
- **Auth:** Public (no JWT). Optional API key (see below).
- **Content-Type:** `application/json`
- **Timezone for all date logic:** `Asia/Dhaka`
- **Data source:** DSE IMDS tables (`imds_idx_data`, `imds_trd_data`).
- **Indices returned:** `dsex` (DSEX), `ds30` (DS30), `dses` (DSES).
---
## Authentication (optional)
If the backend has `SYNC_API_KEY` set in its `.env`, every request MUST include the key:
- Header: `X-API-Key: <key>`, **or**
- Query param: `?api_key=<key>`
Behavior:
| SYNC_API_KEY on server | Request key | Result |
|------------------------|-------------|--------|
| Not set | — | Allowed |
| Set | Matches | Allowed |
| Set | Missing/wrong | `401 Unauthorized` |
**Common failure:** external server gets `401` because the backend enabled `SYNC_API_KEY` but the client isn't sending `X-API-Key` / `api_key`.
---
## Query Parameters
| Param | Format | Required | Default | Purpose |
|-------|--------|----------|---------|---------|
| `date` | `Y-m-d` | No | auto-resolved | Force a specific session date |
| `since` | `Y-m-d H:i:s` | No | — | Incremental mode: return rows **after** this timestamp |
| `tail` | int 1–5000 | No | `200` | Tail mode: return last N intraday points |
| `limit` | int 1–5000 | No | `500` (since mode) / `tail` (tail mode) | Max rows returned |
Validation failure → `422` with an `errors` object. Note `since` must be a **full datetime** `Y-m-d H:i:s` (a bare date will fail validation).
### Modes
- **`since` provided → incremental (`since`) mode:** returns points with `timestamp > since`, ascending, capped by `limit`.
- **No `since` → tail mode:** returns the last `tail` points (default 200), ordered oldest → newest.
---
## Date Resolution (important for debugging)
When `date` is **omitted**, the server resolves the session date using **server time (Asia/Dhaka)** and **data presence** — NOT a live market-open check:
1. **Explicit `date` given** → use it. `date_source = "explicit"`
2. **Now < 09:30 Dhaka** → previous trading day. `date_source = "last_market_day_pre_open"`
3. **Now ≥ 09:30 but no rows for today** → previous trading day. `date_source = "last_market_day_no_today_data"`
4. **Otherwise** → today. `date_source = "today"`
Trading days = **Sunday–Thursday**. (One hardcoded exception: `2026-05-23`, a Saturday, is treated as a trading day.) Public holidays that fall on Sun–Thu are only skipped implicitly via the "no data for today" fallback.
### Practical outcomes when calling without `date`
| Scenario | Resolved date | `date_source` |
|----------|---------------|---------------|
| After close on a trading day (data exists) | Today | `today` |
| Friday / Saturday | Last trading day (e.g. Thursday) | `last_market_day_no_today_data` |
| Before 09:30 any day | Previous trading day | `last_market_day_pre_open` |
| Trading day but ingestion failed / no rows | Previous trading day | `last_market_day_no_today_data` |
**Always inspect `meta.date` and `meta.date_source`** to know which session you actually received.
---
## Response Shape
```json
{
  "success": true,
  "meta": {
    "date": "2026-07-02",
    "calendar_date": "2026-07-02",
    "date_source": "today",
    "timezone": "Asia/Dhaka",
    "mode": "tail",
    "since": null,
    "next_since": "2026-07-02 14:30:00",
    "limit": 200,
    "tail": 200
  },
  "data": {
    "dsex": {
      "overall_data": {
        "price": 6234.56,
        "change": 12.34,
        "change_per": 0.20,
        "open": 6220.00,
        "high": 6250.00,
        "low": 6210.00,
        "total_value": 500000000,
        "total_traded": 15000,
        "total_volume": 25000000
      },
      "realtime_data": [
        { "value": 6220.00, "time": "10:00:00", "timestamp": "2026-07-02 10:00:00" },
        { "value": 6234.56, "time": "14:30:00", "timestamp": "2026-07-02 14:30:00" }
      ]
    },
    "ds30": { "overall_data": { "...": "no totals" }, "realtime_data": [] },
    "dses": { "overall_data": { "...": "no totals" }, "realtime_data": [] }
  }
}
```
### `meta` fields
| Field | Meaning |
|-------|---------|
| `date` | Session date actually queried |
| `calendar_date` | Real current date (Dhaka), for comparison |
| `date_source` | Why that date was chosen (see resolution table) |
| `timezone` | Always `Asia/Dhaka` |
| `mode` | `since` or `tail` |
| `since` | Echoes request `since` (only in since mode) |
| `next_since` | Latest timestamp returned; use as `since` on next poll |
| `limit` | Effective row cap applied |
| `tail` | Effective tail size (only in tail mode) |
### `overall_data` (per index)
| Field | Source | Notes |
|-------|--------|-------|
| `price` | last `IDX_CAPITAL_VALUE` | latest / close value |
| `change` | `IDX_DEVIATION` | absolute change |
| `change_per` | `IDX_PERCENTAGE_DEVIATION` | percent change |
| `open` | first value of day | |
| `high` | max value of day | |
| `low` | min value of day | |
| `total_value` | `imds_trd_data` | **DSEX only** |
| `total_traded` | `imds_trd_data` | **DSEX only** |
| `total_volume` | `imds_trd_data` | **DSEX only** |
Any field can be `null` if no matching row exists for the resolved date.
### `realtime_data` (per index)
Array of points, each:
```json
{ "value": 6234.56, "time": "14:30:00", "timestamp": "2026-07-02 14:30:00" }
```
- Tail mode: last N points ordered oldest → newest.
- Since mode: points after `since`, ascending, up to `limit`.
---
## Recommended Consumption Pattern
1. **Initial load:** `GET /api/index-snapshot` (or with `?tail=1000` for more history). Read `meta.date` / `meta.date_source`.
2. **Incremental polling:** on each poll, send `?since=<previous meta.next_since>`. Only new points return.
3. **If `next_since` is unchanged / no new rows:** safe to reuse the same `since` next time (server returns it back).
---
## HTTP Status Codes
| Code | Meaning |
|------|---------|
| `200` | Success (`success: true`) |
| `401` | API key required/invalid (`SYNC_API_KEY` mismatch) |
| `422` | Validation error (bad `date`/`since`/`tail`/`limit` format) |
| `500` | Server error (`success: false`, message in `error`) |
---
## Debugging Checklist (for the external server)
- [ ] **Getting 401?** Backend has `SYNC_API_KEY` set — send `X-API-Key` header or `?api_key=`.
- [ ] **Getting 422?** Check `since` is full `Y-m-d H:i:s`, and `tail`/`limit` are ints in 1–5000.
- [ ] **"Wrong day" data?** Read `meta.date` + `meta.date_source`. The server resolves by **Dhaka time + data presence**, not live market status. Weekends/holidays/pre-open all fall back to the last trading day.
- [ ] **Empty `realtime_data` but valid `overall_data`?** Possible if points exist for OHLC calc but the tail/since window returned nothing — check `mode`, `tail`, `since`.
- [ ] **All fields null for an index?** No rows for that index on the resolved date.
- [ ] **Only DSEX has totals?** Expected — `total_*` come from `imds_trd_data` and are DSEX-only by design.
- [ ] **Timezone mismatch?** External server must reason in `Asia/Dhaka`; `meta.timezone` and `meta.calendar_date` confirm server clock.
- [ ] **Incremental not advancing?** Ensure you pass the previous response's `meta.next_since` as the next `since`.
---
## Example Requests
```http
# Initial load (auto date, last 200 points)
GET /api/index-snapshot
# Force a date
GET /api/index-snapshot?date=2026-07-01
# More history
GET /api/index-snapshot?tail=1000
# Incremental poll
GET /api/index-snapshot?since=2026-07-02 14:25:00&limit=500
# With API key
GET /api/index-snapshot?api_key=YOUR_KEY
# or header:
#   X-API-Key: YOUR_KEY
```