An LLM on its own is a very smart colleague locked in a windowless room. It can reason beautifully, but it can't watch a YouTube video, read a competitor's pricing page, or look up what a link actually says before clicking it. Function calling (a.k.a. tool use) is the door out of that room — and the quality of the tools you hand your agent determines how useful it becomes.
This tutorial shows you how to turn the four FetchAPI endpoints into native agent tools using OpenAI-style function calling. By the end you'll have a working agent loop that can transcribe videos, read any web page as markdown, extract link metadata, and pull video stats — all from one API key, with no HTML parsing or scraping infrastructure.
Function calling works best when each tool has a narrow, predictable output. You don't want a tool that returns raw HTML (thousands of tokens of noise) or a tool that behaves differently on every site. You want small, structured JSON your model can reason over cheaply.
That's exactly what FetchAPI returns:
| Agent capability | Endpoint | What the model gets |
|---|---|---|
| Read a web page | GET /v1/markdownify |
Clean markdown + word_count |
| Transcribe a video | GET /v1/transcript |
Timestamped segments |
| Preview a link | GET /v1/ogsnap |
Title, description, image |
| Get video metadata | GET /v1/info |
Title, channel, views, duration |
Every endpoint is a plain GET with a url parameter, returns compact JSON, and needs no signup on the free tier — which means you can prototype an agent tool in seconds.
Before writing schemas, it helps to see the actual responses. Each of these is a single curl away.
Transcribe a video:
curl "https://fetchapi.tech/v1/transcript?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ"
{
"video_id": "dQw4w9WgXcQ",
"title": "Rick Astley - Never Gonna Give You Up (Official Video) (4K Remaster)",
"channel": "Rick Astley",
"duration_seconds": 213,
"transcript": [
{ "start": 21.8, "end": 25.95, "text": "We're no strangers to love...", "start_formatted": "0:21" }
]
}
Read a page as markdown:
curl "https://fetchapi.tech/v1/markdownify?url=https://example.com"
{
"content": "This domain is for use in documentation examples...",
"title": "",
"url": "https://example.com",
"word_count": 25,
"char_count": 156
}
Preview a link:
curl "https://fetchapi.tech/v1/ogsnap?url=https://example.com"
{ "url": "https://example.com", "title": "Example Domain", "description": "", "image": "", "og": {}, "twitter": {}, "jsonld": [] }
Get video metadata: GET /v1/info?url=... returns the same header fields as the transcript response minus the segments — perfect for a cheap "should I even read this?" check.
Hand these four schemas to your model. Descriptions matter: they're how the model decides when to call each tool.
TOOLS = [
{
"type": "function",
"function": {
"name": "get_video_transcript",
"description": "Get the full timestamped transcript of a YouTube video. Use when the user asks about what is said in a video.",
"parameters": {
"type": "object",
"properties": {"url": {"type": "string", "description": "YouTube video URL"}},
"required": ["url"],
},
},
},
{
"type": "function",
"function": {
"name": "read_webpage",
"description": "Fetch any web page as clean markdown. Use to read articles, docs, or blog posts.",
"parameters": {
"type": "object",
"properties": {"url": {"type": "string"}},
"required": ["url"],
},
},
},
{
"type": "function",
"function": {
"name": "get_link_preview",
"description": "Get the title, description, and image for a URL. Use to triage links before reading them fully.",
"parameters": {
"type": "object",
"properties": {"url": {"type": "string"}},
"required": ["url"],
},
},
},
{
"type": "function",
"function": {
"name": "get_video_info",
"description": "Get metadata (title, channel, views, duration) for a YouTube video.",
"parameters": {
"type": "object",
"properties": {"url": {"type": "string"}},
"required": ["url"],
},
},
},
]
Each tool is a thin wrapper around one HTTP call. Keep them dumb and predictable:
import requests
BASE = "https://fetchapi.tech/v1"
def _get(path: str, url: str) -> dict:
r = requests.get(f"{BASE}/{path}", params={"url": url}, timeout=20)
r.raise_for_status()
return r.json()
def get_video_transcript(url: str, **_):
data = _get("transcript", url)
text = " ".join(seg["text"] for seg in data.get("transcript", []))
return {"title": data.get("title"), "transcript": text[:12000]}
def read_webpage(url: str, **_):
data = _get("markdownify", url)
return {"title": data.get("title"), "content": data["content"][:12000],
"word_count": data.get("word_count")}
def get_link_preview(url: str, **_):
data = _get("ogsnap", url)
return {"title": data.get("title"), "description": data.get("description"),
"image": data.get("image")}
def get_video_info(url: str, **_):
return _get("info", url)
DISPATCH = {
"get_video_transcript": get_video_transcript,
"read_webpage": read_webpage,
"get_link_preview": get_link_preview,
"get_video_info": get_video_info,
}
Note the truncation on content and transcript. Tools should return enough to answer the question, not the entire internet. A 90-minute video is ~15,000 words; blindly stuffing that into context burns tokens and buries the signal. Truncating, or chunking and retrieving, keeps the agent fast and cheap.
The classic function-calling loop is small: ask the model, run any tools it requests, append the results, ask again. Repeat until it answers.
import json
from openai import OpenAI
client = OpenAI()
def run_agent(user_message: str, model: str = "gpt-4o-mini"):
messages = [{"role": "user", "content": user_message}]
while True:
resp = client.chat.completions.create(
model=model, messages=messages, tools=TOOLS, tool_choice="auto"
)
msg = resp.choices[0].message
messages.append(msg)
if not msg.tool_calls:
return msg.content
for call in msg.tool_calls:
fn = DISPATCH[call.function.name]
args = json.loads(call.function.arguments)
try:
result = fn(**args)
payload = json.dumps(result)
except Exception as e:
payload = json.dumps({"error": str(e)})
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": payload,
})
print(run_agent(
"Summarize this video and tell me if its author has a blog worth reading: "
"https://www.youtube.com/watch?v=dQw4w9WgXcQ"
))
Run that and you'll watch the agent call get_video_info, then get_video_transcript, then reason over the results — a real research loop, not a prompt template.
The most valuable pattern is cheap tool first, expensive tool second. When a user drops a list of links, call get_link_preview (OGSnap) on all of them. It's tiny and fast. The model reads the titles and descriptions, decides which two or three are actually relevant, and only then calls read_webpage (Markdownify) or get_video_transcript on those.
This one ordering rule cuts token spend dramatically and makes the agent feel decisive instead of noisy. It's the difference between an assistant that reads everything and one that reads the right thing.
youtube.com/youtu.be and hand the model the transcript tool; everything else goes to read_webpage. You can encode this in the tool descriptions, or do it in your dispatch layer.try/except and return {"error": ...}. A well-behaved model will retry with a corrected URL or apologize gracefully — much better than a crashed agent run.word_count so the model knows what it didn't see and can ask for more.Function calling turns a chat model into an agent — but only if its tools return clean, structured, right-sized data. FetchAPI's Transcript, Markdownify, OGSnap, and Info endpoints are purpose-built for exactly that: one GET, compact JSON, no scraping stack to maintain.
Start with a single tool (read_webpage is the highest-leverage), get the loop working, then add the rest. Try it now:
curl "https://fetchapi.tech/v1/markdownify?url=https://example.com"
Then browse the full API docs and give your agent the web.