Data Sources

API Data Source Integration

Build an API that MuChat can sync: response fields, pagination, ordering, date filtering, authentication and Laravel, Express and FastAPI examples.

Connect your application, product catalog or knowledge base to MuChat through an API data source. This guide explains the endpoint you need to provide, how MuChat validates it and how to implement it in PHP Laravel, Node.js Express or Python FastAPI.

MuChat reads this endpoint to import and sync knowledge. To add the connection in the dashboard, see Connecting an API.

API requirements

API endpoint

Provide a complete HTTP or HTTPS URL reachable from MuChat's servers. Use HTTPS when sending authentication credentials.

Reachable examplesLocal addresses MuChat cannot reach
https://api.example.com/itemshttp://localhost:3000/api/items
https://example.com/api/productshttp://127.0.0.1/api/items
https://data.example.org/endpointhttp://192.168.1.100/api/items

An endpoint on a private network must be made reachable from MuChat before connecting it. Use the final URL that returns JSON directly, without a redirect. The connection validator follows redirects, but the synchronization client does not. An HTML sign-in page or a browser challenge is not a compatible response.

HTTP method and caching

Your endpoint must support GET requests with these characteristics:

  • Accept pagination, ordering and date filters through the query string.
  • Accept the _nonce query parameter MuChat adds to avoid stale cached responses.
  • Return consistent results for the same filters and ordering while the underlying data is unchanged.
  • Return a successful JSON response within the connection timeout; validation currently uses 30 seconds per request.

MuChat sends no-cache headers as well as _nonce. Your endpoint can ignore the value of _nonce, but must accept it and keep it out of the data filters.

Response format

Return a JSON object with total_count and items:

response.json
{
  "total_count": 100,
  "items": [
    {
      "id": "123",
      "title": "Product Name",
      "date_modified": "2026-10-20T15:30:45.000Z",
      "description": "Detailed product description...",
      "url": "https://example.com/products/123",
      "price": 29.99,
      "regular_price": 39.99,
      "sale_price": 29.99,
      "stock_status": "instock",
      "currency": "USD",
      "variations": [
        { "id": "456", "title": "Size L", "price": 32.99 }
      ]
    }
  ]
}

items contains the requested page. total_count is the integer number of all matching items after filtering, before skip and take. For example, a page of one item can have total_count: 100. A filter with no matches returns {"total_count": 0, "items": []} with a successful response.

Field requirements

Required fields

Every item must include these fields:

FieldTypeRequirementExample
idstring or integerA stable, unique identifier for the item"123" or 456
titlestringA nonempty title"Product Name"
date_modifiedstringExact UTC timestamp with milliseconds"2026-10-20T15:30:45.000Z"

Keep an item's id unchanged between syncs. Update date_modified whenever its content changes, including a price, stock or variation update. Use the actual modification time rather than generating a new timestamp on every GET request.

Exact date format

The required format is YYYY-MM-DDTHH:mm:ss.SSSZ. The final Z means UTC and the fractional part has exactly three digits.

ValueResult
2026-10-20T15:30:45.000ZValid
2026-10-20T15:30:45ZInvalid: milliseconds are missing
2026-10-20 15:30:45Invalid: the separator and timezone are missing
2026-10-20T15:30:45+00:00Invalid: use milliseconds and the Z suffix
2026-10-20T15:30:45.000000ZInvalid: six fractional digits instead of three

Convert timestamps to UTC before formatting them. Do not append Z to an unconverted local time. Keep database modification times at millisecond precision so the returned timestamp and strict date filter agree.

Optional fields

Include the content MuChat should learn in the same item. These fields cover common product and knowledge-base use cases; MuChat also retains product fields such as pricing, stock, currency and variations in the item's metadata.

FieldTypeDescription
descriptionstringDetailed item content
urlstringThe item's related public URL
pricenumberCurrent price
regular_pricenumberPrice before discounts
sale_pricenumberDiscounted price, when applicable
stock_statusstringFor example, instock, outofstock, onbackorder or your own value
currencystringFor example, USD, EUR, IRR, ریال or تومان
variationsarrayProduct variants with their own details
Custom fieldsany JSON valueAdditional product, article or Q&A information

Custom values can be strings, numbers, booleans, objects, arrays or null. They are included in the item's source content; they do not automatically create new search-filter controls in the dashboard. Importing and syncing require API-source access and available knowledge-base capacity in your account.

Variations

Each variation can contain your own fields. Common examples are:

FieldTypeDescription
idstring or integerUnique variation identifier
titlestringVariation display name
pricenumberCurrent variation price
regular_pricenumberRegular price
sale_pricenumberDiscounted price
stock_statusstringStock status
stocknumberAvailable quantity
Custom fieldsany JSON valueColor, size, SKU, weight or other attributes

Example 1: an e-commerce product

product.json
{
  "id": "123",
  "title": "Wireless Headphones",
  "date_modified": "2024-03-20T15:30:45.000Z",
  "description": "High-quality wireless headphones...",
  "price": 99.99,
  "regular_price": 129.99,
  "sale_price": 99.99,
  "stock_status": "instock",
  "currency": "USD",
  "category": "electronics",
  "brand": "TechBrand",
  "sku": "WH-123",
  "weight": 0.3,
  "variations": [
    {
      "id": "123-black-large",
      "title": "Wireless Headphones - Black Large",
      "price": 99.99,
      "regular_price": 129.99,
      "sale_price": 99.99,
      "stock_status": "instock",
      "stock": 25,
      "color": "black",
      "size": "large",
      "sku": "WH-123-BL",
      "weight": 0.35
    },
    {
      "id": "123-white-medium",
      "title": "Wireless Headphones - White Medium",
      "price": 104.99,
      "regular_price": 129.99,
      "sale_price": 104.99,
      "stock_status": "outofstock",
      "stock": 0,
      "color": "white",
      "size": "medium",
      "sku": "WH-123-WM",
      "expected_restock": "2024-04-15"
    },
    {
      "id": "123-red-small",
      "title": "Wireless Headphones - Red Small",
      "price": 94.99,
      "regular_price": 129.99,
      "sale_price": 94.99,
      "stock_status": "onbackorder",
      "stock": 5,
      "color": "red",
      "size": "small",
      "sku": "WH-123-RS",
      "pre_order": true
    }
  ]
}

Example 2: a Q&A knowledge-base item

question.json
{
  "id": 1,
  "title": "What is Python?",
  "date_modified": "2024-03-15T12:00:00.000Z",
  "question": "What is Python?",
  "answer": "Python is a high-level, interpreted programming language known for its simplicity and readability."
}

Both examples are individual items. Return them inside the response's items array and calculate total_count for the complete matching collection.

Query parameters

Your endpoint must implement the following parameters. Requests may omit them, so provide defaults as shown in the implementation examples.

Pagination

ParameterTypeRequired behaviorExample
takepositive integerMaximum number of items to return?take=30
skipnonnegative integerNumber of items to skip in the ordered, filtered result?skip=60

MuChat currently requests up to 31 items at a time during validation and synchronization. A page-size cap of 30 is too small. The examples allow up to 100 items.

Ordering

ParameterTypeRequired behaviorExample
orderstringApply ASC or DESC to the requested fields?order=ASC
order_bystringSort by comma-separated field names?order_by=date_modified,id

Support both date_modified and id. If several items have the same timestamp, id provides stable ordering between them. Sort before applying skip and take.

Map public field names to the corresponding database columns. For example, the public date_modified may come from SQL updated_at, while a MongoDB item's public id may come from _id. Accept only supported sort fields.

Date filtering

ParameterTypeRequired behaviorExample
modified_afterUTC timestamp stringReturn only items with a strictly later modification time?modified_after=2026-10-20T15:30:45.000Z

Apply date_modified > modified_after, with greater than, not greater than or equal. Recalculate total_count after this filter. If no item is newer, return an empty items array and total_count: 0.

Accept both UTC input forms, with and without milliseconds: 2026-10-20T15:30:45.000Z and 2026-10-20T15:30:45Z. The connection check sends milliseconds; synchronization can send whole seconds. Item timestamps in the response must still use the exact millisecond format.

Synchronization creates new items and updates existing ones by id. An existing item with the same date_modified is skipped, even if another field has changed. Removing an item from your API response does not automatically delete its previously imported source; remove that source from the dashboard when needed.

Cache busting

ParameterTypeBehaviorExample
_noncestringAdded automatically by MuChat; accept it without filtering items by its value?_nonce=1792497600000

Authentication methods

Configure authentication when adding the API source in Knowledge Base. The credentials belong to your endpoint. MuChat sends them when reading it.

No authentication

Choose Without authentication when your endpoint is public. MuChat will not add API-key or Bearer credentials.

API key

Provide both the key and its header name, for example X-API-Key. If your endpoint uses Authorization as a custom API-key header, enter the complete value it expects.

API key request
GET /api/items?take=10&skip=0&_nonce=1792497600000 HTTP/1.1
Host: api.example.com
X-API-Key: your-api-key-here

Bearer token

Choose Bearer Token and enter the token itself. MuChat adds the Bearer prefix to the Authorization header.

Bearer request
GET /api/items?take=10&skip=0&_nonce=1792497600000 HTTP/1.1
Host: api.example.com
Authorization: Bearer your-token-here

The endpoint must reject missing or invalid credentials when authentication is enabled. A browser login cookie is not passed to your endpoint by this connection.

Validation requirements

When you submit the API source, MuChat checks the response format, pagination, ordering and date filtering before creating it.

1. Basic structure

  • The response is a JSON object with an integer total_count and an items array.
  • Every returned item is a JSON object. Its id, title and date_modified follow the requirements above.
  • A request with take=1 returns an item whose structure can be checked.

Have at least 2 items available when connecting the source. MuChat requests two items to verify pagination and ordering. Empty arrays are valid when a date filter has no matches, but an initially empty collection cannot pass the connection check.

2. Pagination accuracy

Pagination checks
GET /api/items?take=2&order=ASC&order_by=date_modified,id
GET /api/items?take=1&skip=0&order=ASC&order_by=date_modified,id
GET /api/items?take=1&skip=1&order=ASC&order_by=date_modified,id

The second request must return the first item from the first response; the third must return the second item. Do not return the same first page for every skip value.

For collections with at least 31 items, MuChat can also check overlapping pages: the last item from take=31&skip=0 must match the first item from take=31&skip=30, with the same ordering.

3. Ordering functionality

Ordering checks
GET /api/items?take=2&order=ASC&order_by=date_modified,id
GET /api/items?take=2&order=DESC&order_by=date_modified,id
GET /api/items?take=1&order=ASC&order_by=date_modified,id

Both ascending and descending ordering must work, including multiple fields. Use the same ordering for page requests so items do not move between pages while the data is unchanged.

4. Date filtering precision

MuChat compares requests using these date boundaries:

FilterExpected result
One minute before the oldest item's timestampThe oldest item is still returned first in ascending order
Exactly the oldest item's timestampNo item with that timestamp or an earlier one is returned
The current timeNo items are returned when all modification times are in the past

Keep modification timestamps consistent with the actual time of each change. A future timestamp can fail the last check.

Implementation examples

These examples implement the same response, filtering, ordering and pagination contract. The Laravel example uses an existing SQL Item model; Express uses an existing Mongoose Item model; FastAPI reads the provided JSON file.

PHP Laravel

Place this controller in app/Http/Controllers/DatasourceController.php. The Item model should expose title, description, price and an Eloquent date-cast updated_at stored in UTC with millisecond precision.

DatasourceController.php
<?php

namespace App\Http\Controllers;

use App\Models\Item;
use Carbon\CarbonImmutable;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class DatasourceController extends Controller
{
    public function index(Request $request)
    {
        $params = $request->validate([
            'take' => ['sometimes', 'integer', 'min:1', 'max:100'],
            'skip' => ['sometimes', 'integer', 'min:0'],
            'order' => ['sometimes', 'in:ASC,DESC'],
            'order_by' => ['sometimes', 'string'],
            'modified_after' => [
                'sometimes', 'date',
                'regex:/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/',
            ],
        ]);
        $columns = ['date_modified' => 'updated_at', 'id' => 'id'];
        $fields = array_map('trim', explode(',', $params['order_by'] ?? 'date_modified,id'));
        foreach ($fields as $field) {
            if (!isset($columns[$field])) {
                throw ValidationException::withMessages(['order_by' => 'Unsupported field']);
            }
        }
        if (!in_array('id', $fields, true)) {
            $fields[] = 'id';
        }

        $query = Item::query();
        if (isset($params['modified_after'])) {
            $since = CarbonImmutable::parse($params['modified_after'])->utc();
            $query->where('updated_at', '>', $since->format('Y-m-d H:i:s.v'));
        }
        $total = (clone $query)->count();
        foreach (array_unique($fields) as $field) {
            $query->orderBy($columns[$field], $params['order'] ?? 'ASC');
        }
        $items = $query->offset((int) ($params['skip'] ?? 0))
            ->limit((int) ($params['take'] ?? 30))->get();

        return response()->json([
            'total_count' => $total,
            'items' => $items->map(fn ($item) => [
                'id' => (string) $item->id,
                'title' => $item->title,
                'date_modified' => $item->updated_at->copy()->utc()
                    ->format('Y-m-d\TH:i:s.v\Z'),
                'description' => $item->description,
                'price' => $item->price === null ? null : (float) $item->price,
            ])->values(),
        ]);
    }
}

Register the route in routes/api.php:

routes/api.php
use App\Http\Controllers\DatasourceController;
use Illuminate\Support\Facades\Route;

Route::get('/items', [DatasourceController::class, 'index']);

With Laravel's default API prefix, this exposes /api/items. Apply your chosen API-key or Bearer authentication middleware to the route if the endpoint is private. Add other product fields to the returned item as needed. Laravel's query builder documents the ordering and pagination methods used here.

Node.js Express

This router uses Mongoose. Your existing Item model should have a Date field named updated_at, populated when content changes, plus title, description and price. The public id is the string form of MongoDB's _id.

datasource-router.js
import express from "express";
import Item from "./models/item.js";

const router = express.Router();
const columns = { date_modified: "updated_at", id: "_id" };

router.get("/api/items", async (req, res, next) => {
  const {
    take = "30", skip = "0", order = "ASC",
    order_by = "date_modified,id", modified_after
  } = req.query;
  const integers = [take, skip].every((value) =>
    typeof value === "string" && /^\d+$/.test(value)
  );
  const limit = Number(take);
  const offset = Number(skip);
  if (!integers || !Number.isSafeInteger(limit) || limit < 1 || limit > 100
      || !Number.isSafeInteger(offset) || offset < 0) {
    return res.status(400).json({ error: "Invalid take or skip" });
  }
  if (!["ASC", "DESC"].includes(order) || typeof order_by !== "string") {
    return res.status(400).json({ error: "Invalid ordering" });
  }
  const fields = [...new Set(order_by.split(",").map((field) => field.trim()))];
  if (fields.some((field) => !Object.hasOwn(columns, field))) {
    return res.status(400).json({ error: "Unsupported order_by field" });
  }
  if (!fields.includes("id")) fields.push("id");

  const filter = {};
  if (modified_after !== undefined) {
    const since = typeof modified_after === "string" ? new Date(modified_after) : null;
    if (!since || Number.isNaN(since.getTime())) {
      return res.status(400).json({ error: "Invalid modified_after timestamp" });
    }
    const canonical = since.toISOString();
    if (![canonical, canonical.replace(".000Z", "Z")].includes(modified_after)) {
      return res.status(400).json({ error: "Use UTC ISO 8601 with seconds or milliseconds" });
    }
    filter.updated_at = { $gt: since };
  }
  const sort = Object.fromEntries(fields.map((field) => [
    columns[field], order === "DESC" ? -1 : 1
  ]));
  try {
    const [total_count, items] = await Promise.all([
      Item.countDocuments(filter),
      Item.find(filter).sort(sort).skip(offset).limit(limit).lean()
    ]);
    return res.json({
      total_count,
      items: items.map((item) => ({
        id: String(item._id),
        title: item.title,
        date_modified: item.updated_at.toISOString(),
        description: item.description,
        price: item.price
      }))
    });
  } catch (error) {
    next(error);
  }
});

export default router;

Mount the router in your Express application:

app.js
import datasourceRouter from "./datasource-router.js";

app.use(datasourceRouter);

For a private endpoint, mount your API-key or Bearer middleware before this router. Add any other product fields to the output mapping. See the Mongoose query API for sort, skip, limit and countDocuments.

Python FastAPI

Save this as main.py. The route reads items.json from the same directory, preserves the item's custom fields and returns IDs as strings.

main.py
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from secrets import compare_digest
from typing import Literal

from fastapi import Depends, FastAPI, HTTPException, Query, Security
from fastapi.security import APIKeyHeader, HTTPAuthorizationCredentials, HTTPBearer

app = FastAPI()
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
bearer_auth = HTTPBearer(auto_error=False)


def verify_api_key(api_key: str | None = Security(api_key_header)):
    expected = os.environ.get("DATASOURCE_API_KEY")
    if not expected or not api_key or not compare_digest(api_key, expected):
        raise HTTPException(status_code=401, detail="Invalid API key")


def verify_bearer_token(
    credentials: HTTPAuthorizationCredentials | None = Security(bearer_auth),
):
    expected = os.environ.get("DATASOURCE_BEARER_TOKEN")
    if not expected or not credentials or not compare_digest(credentials.credentials, expected):
        raise HTTPException(status_code=401, detail="Invalid bearer token")


def parse_timestamp(value: str) -> datetime:
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        canonical = parsed.astimezone(timezone.utc).isoformat(timespec="milliseconds")
        canonical = canonical.replace("+00:00", "Z")
        if parsed.tzinfo is None or value not in {canonical, canonical.replace(".000Z", "Z")}:
            raise ValueError
        return parsed
    except (TypeError, ValueError):
        raise HTTPException(status_code=422, detail="Use UTC ISO 8601 with seconds or milliseconds")


def load_json_file() -> list[dict]:
    path = Path(__file__).with_name("items.json")
    items = json.loads(path.read_text(encoding="utf-8"))
    return [{**item, "id": str(item["id"])} for item in items]


@app.get("/api/items")
def get_items(
    take: int = Query(default=30, ge=1, le=100),
    skip: int = Query(default=0, ge=0),
    order_by: str = "date_modified,id",
    order: Literal["ASC", "DESC"] = "ASC",
    modified_after: str | None = None,
    # API key authentication: uncomment the next line.
    # api_key: None = Depends(verify_api_key),
    # Bearer authentication: uncomment the next line instead.
    # bearer: None = Depends(verify_bearer_token),
):
    fields = list(dict.fromkeys(field.strip() for field in order_by.split(",")))
    if any(field not in {"date_modified", "id"} for field in fields):
        raise HTTPException(status_code=422, detail="Unsupported order_by field")
    if "id" not in fields:
        fields.append("id")
    items = load_json_file()
    if modified_after is not None:
        since = parse_timestamp(modified_after)
        items = [item for item in items if parse_timestamp(item["date_modified"]) > since]
    for field in reversed(fields):
        items.sort(
            key=lambda item: parse_timestamp(item[field]) if field == "date_modified" else item[field],
            reverse=order == "DESC",
        )
    total_count = len(items)
    return {"total_count": total_count, "items": items[skip:skip + take]}

Choose the authentication method to match the data source configuration:

  • Without authentication: Leave both dependency lines commented.
  • API Key: Set the DATASOURCE_API_KEY environment variable and uncomment the api_key dependency. Configure X-API-Key as the header name in MuChat.
  • Bearer Token: Set DATASOURCE_BEARER_TOKEN and uncomment the bearer dependency instead.

The active authentication dependency rejects a missing token, an incorrect token or a missing server-side secret. FastAPI's security reference explains the header dependencies used in the example.

Put the following sample data in items.json. Two items share a timestamp so you can check the secondary ordering by id:

items.json
[
  {
    "id": "1",
    "title": "Wireless Headphones",
    "date_modified": "2026-10-20T09:30:00.000Z",
    "description": "Wireless headphones with a microphone.",
    "price": 99.99,
    "currency": "USD"
  },
  {
    "id": "2",
    "title": "USB-C Cable",
    "date_modified": "2026-10-20T09:30:00.000Z",
    "description": "A one-meter USB-C charging cable.",
    "price": 9.99,
    "currency": "USD"
  },
  {
    "id": "3",
    "title": "What is Python?",
    "date_modified": "2026-10-21T12:00:00.000Z",
    "question": "What is Python?",
    "answer": "Python is a high-level programming language."
  }
]

Install FastAPI and run the example:

Terminal
pip install "fastapi[standard]"
fastapi dev main.py

Test it locally at http://127.0.0.1:8000/api/items. Before connecting MuChat, deploy it at an externally reachable URL. More examples are available in the MuChat example repository.

Common issues and solutions

Date format errors

Problem: The connection check rejects date_modified.

Solution: Return date_modified in the exact YYYY-MM-DDTHH:mm:ss.SSSZ format. Convert to UTC, keep three fractional digits and update the timestamp whenever the item changes. Accept modified_after both with milliseconds and with whole seconds. Apply the date filter at the same precision as the timestamp you return.

Pagination issues

Problem: The same item appears on every page, items are skipped or validation fails with a larger page.

Solution: Apply the date filter first, sort by date_modified,id, then apply skip and take. Allow a page size of at least 31. Calculate total_count before pagination and keep the sort order stable when timestamps are equal.

Authentication failures

Problem: The endpoint returns 401 or 403.

Solution: Match the header name and value to your endpoint. For Bearer authentication, enter only the token in MuChat. Make sure the server rejects invalid credentials and accepts the configured valid token without requiring a browser session.

Empty collection or an incorrect count

Problem: The initial connection check has no item to validate, or total_count changes with the page size.

Solution: Make at least two items available during connection. Count all records matching the current date filter before applying pagination. When the filter has no matches, return a successful response with items: [] and total_count: 0.

Connection or response errors

Problem: MuChat cannot reach the endpoint, the request times out or the response contains HTML.

Solution: Check the public URL, DNS, network access and authentication. Use the final URL without a redirect. Ensure the GET endpoint returns JSON directly, accepts all query parameters including _nonce and responds within the timeout. Check the response outside your signed-in browser session.

Testing your API

Before connecting the source, use curl or Postman to test basic retrieval, pagination, both ordering directions, date filtering and combined parameters. Replace the example URL and timestamp with your own values.

Basic retrieval and pagination
curl --get 'https://api.example.com/items' --data-urlencode 'take=5'
curl --get 'https://api.example.com/items' --data-urlencode 'take=5' \
  --data-urlencode 'skip=5' --data-urlencode 'order_by=date_modified,id'
Ordering and date filtering
curl --get 'https://api.example.com/items' --data-urlencode 'order=DESC' \
  --data-urlencode 'order_by=date_modified,id'
curl --get 'https://api.example.com/items' \
  --data-urlencode 'modified_after=2026-10-20T09:30:00.000Z'
curl --get 'https://api.example.com/items' \
  --data-urlencode 'modified_after=2026-10-20T09:30:00Z'
Combined parameters with an API key
curl --get 'https://api.example.com/items' \
  --header 'X-API-Key: your-api-key-here' \
  --data-urlencode 'take=31' --data-urlencode 'skip=0' \
  --data-urlencode 'order=ASC' --data-urlencode 'order_by=date_modified,id' \
  --data-urlencode 'modified_after=2026-10-20T09:30:00.000Z' \
  --data-urlencode '_nonce=1792497600000'

For Bearer authentication, replace the API-key header with Authorization: Bearer your-token-here. Repeat the date-boundary checks, and check that missing or invalid credentials are rejected when authentication is enabled.

Once the checks pass, add the URL and authentication details in Data Sources. After changing source content, update date_modified and use resynchronization to retrieve the changes.

MuChat documentation

On this page