Website Sources
List, add, remove and rescrape the website pages scraped into the knowledge base through MCP or REST, and keep them in sync with an external feed of links.
Website sources are the pages Octocom scrapes into a business's knowledge base. Each source is one URL. Scraping turns the page into searchable knowledge for the bot; removing a source also removes the knowledge scraped from it.
Every source has an origin:
| Origin | Meaning |
|---|---|
manual | Added one by one, from the dashboard, MCP, REST or a feed sync. Once scraped, it refreshes about once a day |
sitemap | Belongs to a sitemap configured in the dashboard. Its weekly sitemap check adds, removes and rescrapes them |
And a status:
| Status | Meaning |
|---|---|
scraped | The last scrape succeeded at lastScrapedAt |
not_scraped | No scrape has succeeded yet. The page may still be in the queue, or fetching it failed (for example a 403, a 404 or a redirect loop). A page that has never scraped is not retried until you rescrape it |
URLs are stored exactly as given. https://shop.example/a and
https://shop.example/a/ are two different sources. Only http and https
URLs are accepted.
MCP tools
| Tool | Purpose |
|---|---|
list_website_sources | List sources by URL with origin and status. Pass nextCursor back as cursor to page |
add_website_sources | Add up to 500 pages and queue a scrape for each new one. Existing URLs are not rescraped |
set_website_sources | Make the manual sources exactly this list (up to 2,000) in one call; dryRun: true previews |
remove_website_sources | Remove sources by ids and/or exact urls, with their scraped knowledge |
rescrape_website_sources | Queue a fresh scrape of up to 100 existing sources |
REST operations
All paths below start with /rest/v1 and require X-API-Key. Use the
REST reference for complete schemas. Copilot and
MCP can discover these endpoints in the Website Sources group with
list_rest_api_operations.
| Method | Path | Purpose |
|---|---|---|
| GET | /website-sources?businessSlug=shop | List sources; optional origin, limit (up to 500) and cursor |
| POST | /website-sources | Add {businessSlug, urls}; returns added and alreadyPresent URLs |
| PUT | /website-sources | Set the exact manual list {businessSlug, urls, dryRun?} |
| POST | /website-sources/remove | Remove {businessSlug, ids?, urls?}; returns removed and notFound |
| POST | /website-sources/rescrape | Rescrape {businessSlug, ids?, urls?}; returns queued and notFound |
Sources are scoped to the API key's organization and the requested business.
IDs or URLs from another business are reported as notFound and never touched.
Archived businesses remain readable but reject changes with 409, and their
pages are not refreshed.
Scrapes run in a shared, rate-limited queue. A manual page that fails during
its daily refresh keeps its last good content and is tried again a day later.
Add or rescrape only pages whose content you actually need refreshed.
Keep website knowledge in sync with a feed
The set operation (PUT /website-sources or set_website_sources) compares
the list you send with the business's current manual sources:
- URLs that are new are added and scraped.
- Manual sources missing from the list are removed, together with their scraped knowledge.
- Everything else is left untouched and is not rescraped.
Sitemap sources are never added or removed by this call. A URL that already exists as a sitemap source counts as present. An empty list is rejected, so a failed feed download cannot wipe the knowledge base by accident.
A Python recurring job can run
the sync daily with its injected OCTOCOM_ORG_API_KEY and
OCTOCOM_API_BASE_URL; no extra secret is needed. Fetch and validate the whole
feed first, and stop without changing anything when it looks wrong:
import xml.etree.ElementTree as ET
import requests
FEED_URL = "https://shop.example/feeds/links.xml"
BUSINESS_SLUG = "shop"
def run_job(context):
response = requests.get(FEED_URL, timeout=30)
response.raise_for_status()
links = [el.text.strip() for el in ET.fromstring(response.content).iter("LINK") if el.text and el.text.strip()]
if not links:
raise RuntimeError("Feed returned no links; keeping the current sources")
result = requests.put(
f"{OCTOCOM_API_BASE_URL}/rest/v1/website-sources",
headers={"X-API-Key": OCTOCOM_ORG_API_KEY},
json={"businessSlug": BUSINESS_SLUG, "urls": links},
timeout=60,
)
result.raise_for_status()
diff = result.json()["data"]
return {"added": len(diff["added"]), "removed": len(diff["removed"]), "unchanged": diff["unchanged"]}Run it once with "dryRun": True to check the difference before the first
real sync. A second run against an unchanged feed reports zero additions and
removals.