# ILovePSX Funds API — developer guide

Updated: 2026-10-09. Base URL: https://funds.api.ilovepsx.com. API major version: v1.

Canonical Pakistani mutual fund registry, dated NAV, observed payouts and validated monthly Fund Manager Report data. This is public documentation, not live fund data.

## Your first request

Start with the registry. It gives you the fund slugs to use in every later request.

1. Sign in using your existing ILovePSX account and explicitly enable fund API access. Activation requires your verified primary email, WhatsApp number, intended use and terms acknowledgement.
2. Open Dashboard → API keys. Create a key with funds:read, then add nav:read or fmr:read for the data you need. Save the secret when it is shown once.
3. Set ILOVEPSX_API_KEY in your server's environment or secret manager. The examples read it at runtime. Never commit the value or expose it through NEXT_PUBLIC_* variables.
4. Run the registry request below. A 200 response contains success, data and meta. Choose a data[].slug, then use that exact slug in NAV, report or payout requests.

The cURL examples use Bash/POSIX syntax. For PowerShell, Python and JavaScript, use the language selector. Python needs httpx; JavaScript examples run on a modern Node.js server.

### PowerShell · first request

```powershell
$headers = @{ "X-API-Key" = $env:ILOVEPSX_API_KEY }
$response = Invoke-RestMethod -Headers $headers -Uri "https://funds.api.ilovepsx.com/v1/mutual-funds/funds?per_page=10"
$response.data | Select-Object name, slug
```

### cURL

```bash
curl --fail-with-body --request GET \
  --header "X-API-Key: $ILOVEPSX_API_KEY" \
  "https://funds.api.ilovepsx.com/v1/mutual-funds/funds?per_page=10"
```

### Python

```python
# Install: python -m pip install httpx
import os
import httpx

response = httpx.get(
    "https://funds.api.ilovepsx.com/v1/mutual-funds/funds?per_page=10",
    headers={"X-API-Key": os.environ["ILOVEPSX_API_KEY"]},
    timeout=30,
    follow_redirects=False,
)
if response.status_code == 304:
    raise RuntimeError("Use your previously cached response")
response.raise_for_status()
payload = response.json()
data, meta = payload["data"], payload["meta"]
```

### JavaScript

```javascript
// Run on your server with Node.js; keep the key out of browser code.
const key = process.env.ILOVEPSX_API_KEY;
if (!key) throw new Error("Set ILOVEPSX_API_KEY");
const response = await fetch("https://funds.api.ilovepsx.com/v1/mutual-funds/funds?per_page=10", {
  method: "GET",
  headers: { "X-API-Key": key },
  redirect: "error",
  signal: AbortSignal.timeout(30000),
});
if (response.status === 304) throw new Error("Use your cached response");
const payload = await response.json();
if (!response.ok) throw new Error(payload.code + ": " + payload.detail);
const { data, meta } = payload;
```

## Choose the right endpoint

Use the registry for identity, NAV for dated prices, and Fund Manager Reports for reported monthly facts.

- [Find funds and AMCs](https://funds.api.ilovepsx.com/docs/reference#list_mutual_funds): Search and filter the canonical registry; retrieve stable public IDs and slugs.
- [Read the latest available NAV](https://funds.api.ilovepsx.com/docs/reference#get_latest_mutual_fund_navs): Use repeated funds query parameters to select fund slugs; inspect each record's nav_date.
- [Collect NAV history](https://funds.api.ilovepsx.com/docs/reference#get_mutual_fund_nav_history): Bound the date range and follow the opaque next_cursor.
- [Read a complete monthly report](https://funds.api.ilovepsx.com/docs/reference#get_latest_complete_fmr_report): Use include=all for expanded reported sections; availability varies by fund and period.
- [Retrieve observed payouts](https://funds.api.ilovepsx.com/docs/reference#get_mutual_fund_payout_history): Use payout_date, amount_per_unit and coverage; an empty result does not prove no distributions.
- [Batch several funds](https://funds.api.ilovepsx.com/docs/reference#post_bulk_mutual_fund_nav): POST a funds list to the bounded bulk endpoints; inspect meta.missing_funds.
- [Discover capabilities](https://funds.api.ilovepsx.com/docs/reference#get_api_catalog): Read the authenticated catalog for supported fields and endpoint capabilities.

The API serves canonical registry/NAV records and active promoted FMR facts. Raw extraction, staging and review controls are private. Derived analytics remain conditional and are excluded from this public reference until verified. Stock data, a hosted MCP server and an official client SDK are not provided here.

## Authentication and scopes

Every data request uses your API key in the X-API-Key header. Reading these docs needs no account.

| Scope | What it allows |
| --- | --- |
| funds:read | Registry, fund profiles, AMCs and classification vocabularies |
| nav:read | Latest/historical NAV, NAV batches and observed payouts |
| fmr:read | Reports, snapshots, reported performance and portfolio sections |
| catalog:read | API catalog, fields, enums and production coverage metadata |
| updates:read | Incremental production update events |

The reference shows the required scope for each endpoint; the key must also belong to an active account. Login sessions manage your account in the portal; use an API key for integrations. Visiting account status does not activate the service or create a key.

Use separate keys for separate integrations. Rotation immediately revokes the previous secret, so update your server secret when rotating. A revoked secret cannot be recovered. Keep keys out of URLs, client bundles, browser storage, logs, screenshots and AI prompts. Use the authenticated explorer to try data requests without pasting a key into these docs.

## Read a response

Successful JSON has a common envelope. Dataset fields inside data depend on the endpoint.

### Synthetic NAV response · fictional fund and values

```json
{
  "success": true,
  "data": [
    {
      "fund_id": "00000000-0000-4000-8000-000000000201",
      "fund_slug": "example-research-fund",
      "fund_name": "Example Research Fund",
      "mufap_fund_id": null,
      "nav_date": "2026-09-30",
      "nav": "100.500000",
      "offer_price": "100.500000",
      "repurchase_price": "100.500000",
      "market_price": null,
      "front_end_load_pct": "0.000000",
      "back_end_load_pct": "0.000000",
      "contingent_load_pct": null,
      "nav_change_abs": null,
      "nav_change_pct": null,
      "currency": "PKR"
    }
  ],
  "meta": {
    "request_id": "00000000000040008000000000000402",
    "generated_at": "2026-10-08T08:00:00Z",
    "data_as_of": "2026-09-30",
    "data_freshness_days": 8
  }
}
```

| Field | Meaning |
| --- | --- |
| success | true for a successful JSON response |
| data | An array or object specific to the endpoint; empty arrays are valid |
| meta.request_id | Correlation ID to retain for debugging and support |
| meta.generated_at | UTC time when the response was constructed; not the source observation date |
| meta.data_as_of | Available source date; inspect row dates too when comparing multiple funds |
| meta.data_freshness_days | Source age when supplied; null means unavailable, not fresh |
| meta.next_cursor | Opaque next-page token, or null when this feed is exhausted |
| meta.missing_funds | Requested slugs missing from a bulk result when this field is present |

The OpenAPI contract describes authentication, parameters, request bodies and the shared envelope. Many dataset data fields are not fully typed in that schema. Do not generate assumptions from the generic data property; use these labelled examples, the detailed guides and the authenticated explorer to inspect actual results.

- [Download the synthetic complete FMR response](https://funds.api.ilovepsx.com/blog/examples/fmr-response.json): All expanded sections, with fictional identifiers/values and no fabricated source document.
- [Download the synthetic NAV response](https://funds.api.ilovepsx.com/blog/examples/nav-response.json): Exact decimal strings and explicit nulls. These are teaching fixtures, not live fund data.

## Decimals, dates and missing values

Preserve the source meaning through storage, calculations and display.

| Convention | Integration rule |
| --- | --- |
| Financial decimals | JSON strings preserve precision. Use Decimal or a decimal library for calculations. |
| Percentages | Percentage points: 5.25 represents 5.25%, not a 0.0525 stored fraction. |
| Dates | YYYY-MM-DD. NAV dates and FMR report dates are different concepts. |
| Timestamps | UTC ISO-8601. Keep source dates separate from generated_at. |
| Currency | NAV prices and payout amounts are PKR; retain currency fields when supplied. |
| null / absent | Unavailable or inapplicable facts; never silently replace them with zero. |
| Identity | Use public UUIDs and registry slugs. Do not invent a slug from a display name. |
| Latest | Latest available production observation, which may be older than today. |

### Python · calculate using exact decimals

```python
from decimal import Decimal

# Illustrative values, not investment advice or observed fund prices.
nav = Decimal("100.500000")
units = Decimal("12.5")
value_pkr = nav * units
assert value_pkr == Decimal("1256.2500000")
print(value_pkr)
```

Do not treat raw NAV change as an investor's total return: distributions, fees, unit adjustments and the chosen observation dates can affect interpretation. Monthly reported returns must retain their report period and original labels.

## Fund Manager Reports and coverage

A valid fund can have no promoted report for a requested month. A valid report can have missing sections.

- List /funds/{fund_slug}/fmr-reports to discover available reporting dates. Use a date returned by that list when requesting a historical report.
- For a complete report, add include=all, or request the supported expansions explicitly. A default report can omit sections that were not requested.
- Keep report_date, data_version, report fragments and supplied provenance together. NAV source dates and FMR periods should not be combined into one freshness label.
- An omitted expansion, NOT_REPORTED section and unavailable report are different states. A section's empty rows do not justify inventing holdings or allocation weights.
- Credit quality is available through its focused endpoint and reported allocations. Do not invent a credit_quality expansion. Portfolio coverage and denominators differ by section.

Extraction goes through validation, staging and matching/review before production promotion. This process does not guarantee complete AMC coverage or that every source fact was reported. For corrections, retain public report versions and process update events idempotently.

| Complete-report include | Sections |
| --- | --- |
| Default | sub_funds, reported_returns, metrics, fees, ratings, parties, source |
| Additional expansions | allocations, holdings, special_terms, compliance, notes |
| all | Every supported complete-report expansion. This does not guarantee populated rows. |
| Example | include=holdings,allocations or repeated include=holdings&include=allocations |

- [FMR integration guide](https://funds.api.ilovepsx.com/fund-manager-reports-api): Reporting periods, provenance and expanded sections.
- [Portfolio and research guides](https://funds.api.ilovepsx.com/blog): Worked examples for holdings, allocations, benchmarks and missing-data interpretation.

## Pagination and synchronization

Follow the pagination style published for the endpoint. Keep the filters stable between pages.

| Style | How to continue |
| --- | --- |
| page / per_page | Start at page=1. Use returned meta.has_next / total_pages and effective per_page. |
| cursor / limit | Pass meta.next_cursor unchanged. Stop when it is null; keep fund, range and order unchanged. |
| Repeated array parameters | Use funds=SLUG_A&funds=SLUG_B or category=VALUE_A&category=VALUE_B, not a comma-joined list unless explicitly supported. |
| Bulk bodies | POST a JSON funds array. Inspect missing_funds rather than assuming every requested fund appears. |

### Python · collect registry pages

```python
import os
import httpx

with httpx.Client(
    base_url="https://funds.api.ilovepsx.com",
    headers={"X-API-Key": os.environ["ILOVEPSX_API_KEY"]},
    timeout=30, follow_redirects=False,
) as client:
    page = 1
    while True:
        response = client.get("/v1/mutual-funds/funds",
                              params={"page": page, "per_page": 100})
        response.raise_for_status()
        payload = response.json()
        for fund in payload["data"]:
            print(fund["slug"])
        if not payload["meta"]["has_next"]:
            break
        page += 1
```

For cursor feeds, replace only cursor in the original query and URL-encode it with your HTTP client. Do not decode, alter or use a cursor for a different filter. Save synchronization checkpoints only after you have processed a page successfully.

## Request bounds and batch reads

Ask for only the data you need. Operational bounds remain active under the free offer.

| Operation | Published default / bound |
| --- | --- |
| Registry lists | per_page defaults to 50; the default service maximum is 100 and returned meta shows the effective size. |
| NAV history | limit defaults to 500; schema maximum 1000. Default maximum date span is 3653 days. |
| Payout history | limit defaults to 100, maximum 500. Default range is 1827 days; maximum span is 1830 days. |
| FMR report lists | page defaults to 1; per_page defaults to 50, maximum 100. |
| Bulk fund selection | Default service maximum 100 fund slugs; required non-empty funds array. |
| Bulk NAV date | date omitted reads latest; date supplied uses the requested date_policy. Bulk from/to fields are not a NAV history interface. |

Schema bounds and deployed service settings are distinct: a request can fit the OpenAPI type but exceed a service bound. Handle a 422 response and inspect the authenticated catalog rather than increasing payloads indefinitely. Do not assume every endpoint supports all filters.

## Free usage and rate limits

The current offer has free unlimited monthly usage, with burst protection per key.

There is no monthly usage bill or payment-card requirement. The current burst limit is 600 weighted units per minute per key, with a separate explorer-session bucket. Units represent endpoint workload. Future pricing will be announced separately.

| Read | Weighted units |
| --- | --- |
| Standard registry, NAV history, payouts and focused FMR reads | 1 |
| Complete FMR and latest complete snapshot | 2 |
| GET NAV bulk / POST fund bulk | 2 |
| POST NAV bulk / VPS family snapshot | 3 |
| POST snapshot bulk | 5 |

| Response header | Action |
| --- | --- |
| RateLimit-Limit | Weighted burst capacity |
| RateLimit-Remaining | Remaining weighted capacity in this bucket |
| RateLimit-Reset | Bucket reset information; do not assume it is a source-data timestamp |
| Retry-After | Wait the indicated duration after a 429 before retrying |
| X-Monthly-Quota-Limit | unlimited under the current free offer |
| X-Request-ID | Retain this ID for failures and support |

Rejected authorization and limit checks do not consume units; server-error reservations are refunded. Successful reads and conditional requests still follow access and burst policy. Activity history can lag enforcement by a few seconds. Avoid synchronized parallel retry storms.

## Caching and resilient requests

Use bounded requests, explicit timeouts and a retry policy that understands the response.

- Where an ETag is supplied, store it with the response and send If-None-Match on the next read. A 304 has no JSON body: reuse your cached data.
- Cache by the complete URL, filters and customer context. Keep authenticated responses in your own private application cache; never expose customer credentials through a shared cache.
- ETags represent a complete response, including changing generation metadata. A subsequent request can return 200 even when the underlying facts look unchanged; do not depend on 304 for correctness.
- For 429, honor Retry-After. For transient 500/503 or network failures, use bounded exponential backoff with jitter and a maximum attempt count. Keep failed batches/checkpoints recoverable.
- Do not retry invalid keys, missing scopes, unavailable funds or invalid input indefinitely. Surface the problem code and request ID. Read-only bulk POSTs query data; they do not write production facts.
- Disable credential-bearing redirects and validate JSON/error responses before using them. Avoid logging headers or full response bodies that could contain customer secrets.

## Troubleshoot with a request ID

Errors use application/problem+json. Inspect the HTTP status and stable code before deciding what to do.

### Synthetic validation error

```json
{
  "type": "https://api.ilovepsx.com/problems/validation-error",
  "title": "Request validation failed",
  "status": 422,
  "detail": "One or more request values are invalid.",
  "instance": "/v1/mutual-funds/funds",
  "request_id": "example-request-id",
  "code": "VALIDATION_ERROR",
  "errors": [
    {
      "location": "query.page",
      "message": "Input should be greater than or equal to 1",
      "type": "greater_than_equal"
    }
  ]
}
```

| Status | Likely cause | Next step |
| --- | --- | --- |
| 400 / 422 | Invalid filters, bounds, date or expansion | Inspect code and errors; correct the request before retrying. |
| 401 | Missing, invalid, inactive or expired API key | Check the server environment and key status; rotate if needed. |
| 403 | Missing scope or unavailable account | Check required scope, activation and account status. |
| 404 | Unknown fund or unavailable production report | Use the registry and report catalog; represent absence explicitly. |
| 429 | Weighted burst capacity exceeded | Wait Retry-After, reduce concurrency and retry within a bound. |
| 500 / 503 | Service or dependency failure | Retry with backoff; retain request ID and UTC timestamp. |

## Use these docs with AI

Give a coding assistant the public contract and integration rules without sharing your credentials.

Copy the AI prompt, or download the guide/reference as Markdown and attach them to your assistant. Assistants with URL-reading tools can begin at /llms.txt. If yours cannot fetch URLs, attach /llms-full.txt or the OpenAPI JSON instead.

### AI integration prompt

```text
Use the ILovePSX Funds API to implement my integration. First read https://funds.api.ilovepsx.com/llms.txt and https://funds.api.ilovepsx.com/docs/index.md, then the relevant operations in https://funds.api.ilovepsx.com/docs/reference.md or import https://funds.api.ilovepsx.com/docs/openapi. Ask me which language and dataset I need. Use only published endpoints. Read ILOVEPSX_API_KEY from a server environment variable; never ask me to paste a secret into chat. Discover fund slugs from the registry. Preserve exact decimal strings, nulls, source dates, FMR reporting periods and provenance. Handle pagination, missing reports, 304, 429 Retry-After and bounded retries. Do not invent coverage, analytics endpoints, SDKs or an MCP server. Clearly label synthetic examples and cite the documentation used. Produce runnable server-side code and explain setup and error handling.
```

- [AI integration resources](https://funds.api.ilovepsx.com/docs/ai): Prompt, compact discovery index, complete context and importable contract.
- [OpenAPI JSON](https://funds.api.ilovepsx.com/docs/openapi): Import into an API client or code generator. Dataset response typing remains partial.
- [Full AI context](https://funds.api.ilovepsx.com/llms-full.txt): One plain-text file containing the guide, endpoint reference and shared schemas.

These resources are documentation. They do not grant account access, run API requests or install a connector. Any live request still needs an active account and an appropriately scoped server-side key. AI-produced code should preserve missing values and source dates and be checked against the published contract.

## Support and next steps

Include enough context to investigate your integration without exposing a secret.

- Send the request ID, method and endpoint, UTC timestamp, status/problem code and a short description. Include your public account ID when relevant.
- Redact full API keys, bearer tokens and customer details from logs or screenshots.
- Use the dashboard for keys, usage and request history; use the explorer for authenticated data inspection. Deeper guides are in the public blog library.

Support: zia639329@gmail.com; WhatsApp +923194583904. Never send a full API key.
