<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Perceptlayer blog</title>
    <link>https://perceptlayer.com/blog</link>
    <description>Guides, changes and notes from the Perceptlayer team.</description>
    <language>en</language>
    <atom:link href="https://perceptlayer.com/blog/rss.xml" rel="self" type="application/rss+xml"/>
    <lastBuildDate>Thu, 22 Oct 2026 00:00:00 GMT</lastBuildDate>
    <item>
      <title>AI Agent API Cost Control: Cap Web Data Spend</title>
      <link>https://perceptlayer.com/blog/ai-agent-web-data-costs</link>
      <guid isPermaLink="true">https://perceptlayer.com/blog/ai-agent-web-data-costs</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
      <description>Cap an agent's web data spend per run: credit arithmetic for 8 and 20 calls per task, a Python budget wrapper, and why failed calls cost 0 credits.</description>
      <category>AI agents</category>
      <category>ai agents</category>
      <category>cost control</category>
      <category>credits</category>
      <category>mcp</category>
      <category>budgeting</category>
      <content:encoded><![CDATA[<h2 id="why-do-agent-data-costs-drift">Why do agent data costs drift?</h2>
<p>Agents decide at run time how many calls a task needs, so the bill depends on model behaviour as much as on traffic. A model that keeps refining a query or loops on an empty result makes more calls than you planned. Per-call pricing is fine. The unbounded loop is the problem, and the fix is a ceiling the loop can&#39;t pass.</p>
<p>Prepaid credits help because the account itself has a ceiling. The service is prepaid: purchased credits last 12 months, with no subscription and no seats. When the balance runs out, calls return <code>insufficient_credits</code> (402) and nothing more is debited, so a runaway agent stops at the balance you loaded. A per-run cap keeps one run from eating that whole balance.</p>
<h2 id="how-do-you-calculate-credits-per-agent-task">How do you calculate credits per agent task?</h2>
<p>Count the calls one task needs, multiply by the credits per call, and keep two figures: the typical case and the capped worst case. For endpoints that cost 1 credit, calls and credits are the same number. Check the cost shown on each endpoint you give the agent, because the cap should be in credits if any endpoint costs more than 1.</p>
<p>Take an example agent that answers a research question. Assume it typically makes 8 calls per task and you cap it at 20. These are example figures for the arithmetic, not measurements. Per run, that&#39;s 8 credits typical and 20 credits worst case.</p>
<table>
<thead>
<tr>
<th>Pack</th>
<th>Price</th>
<th>Credits</th>
<th>Cost per 1,000 calls</th>
<th>Typical run (8 calls)</th>
<th>Capped run (20 calls)</th>
</tr>
</thead>
<tbody><tr>
<td>Free tier</td>
<td>$0</td>
<td>1,000 per month</td>
<td>$0</td>
<td>$0</td>
<td>$0</td>
</tr>
<tr>
<td>Starter</td>
<td>$10</td>
<td>12,500</td>
<td>$0.80</td>
<td>$0.0064</td>
<td>$0.016</td>
</tr>
<tr>
<td>Growth</td>
<td>$50</td>
<td>83,000</td>
<td>$0.60</td>
<td>$0.0048</td>
<td>$0.012</td>
</tr>
<tr>
<td>Scale</td>
<td>$200</td>
<td>400,000</td>
<td>$0.50</td>
<td>$0.004</td>
<td>$0.01</td>
</tr>
</tbody></table>
<p>Run counts follow from the credits. The free tier&#39;s 1,000 credits cover 125 typical runs (1,000 ÷ 8) or 50 capped runs (1,000 ÷ 20). The Starter pack&#39;s 12,500 credits cover 1,562 typical runs or 625 capped runs. At 500 tasks a month, typical spend is 4,000 credits, and the cap bounds the worst case at 10,000 credits, which fits inside the Starter pack&#39;s 12,500.</p>
<h2 id="why-dont-failed-calls-count-against-the-budget">Why don&#39;t failed calls count against the budget?</h2>
<p>A failed call returns an HTTP status, a JSON error body and the header <code>X-Credits-Used: 0</code>. Only a successful response debits credits. So the retries an agent makes after a <code>rate_limited</code> (429, the status <a href="https://www.rfc-editor.org/rfc/rfc6585#section-4">RFC 6585</a> defines for too many requests) or <code>source_error</code> (502) response don&#39;t change the credit total.</p>
<p>Take a run that makes 8 successful calls and hits 3 errors along the way: 8 credits, not 11. The three failed attempts cost nothing in credits. They still cost time and model tokens, so they should count toward a separate attempt cap.</p>
<p>That second cap matters. An agent that retries an <code>invalid_parameters</code> (422) response forever will never be billed for it, and it will never finish either. See the <a href="https://perceptlayer.com/docs#errors">error reference</a> and the <a href="https://perceptlayer.com/docs#limits">rate limit docs</a>.</p>
<h2 id="how-do-you-build-ai-agent-api-cost-control-in-code">How do you build AI agent API cost control in code?</h2>
<p>Put the cap in the function that executes the agent&#39;s tool calls. The wrapper below counts credits and attempts, refuses to send a request that would pass either limit, and raises <code>BudgetExceeded</code> so the agent loop stops and returns what it has. It counts each successful call at the endpoint&#39;s listed cost, which you pass in as <code>credits_per_call</code>.</p>
<pre><code class="language-python">import os
import requests


class BudgetExceeded(Exception):
    pass


class CallBudget:
    def __init__(self, max_credits, max_attempts, credits_per_call=1):
        self.max_credits = max_credits
        self.max_attempts = max_attempts
        self.credits_per_call = credits_per_call
        self.spent = 0
        self.attempts = 0

    def call(self, url, params=None, method=&quot;GET&quot;):
        if self.spent + self.credits_per_call &gt; self.max_credits:
            raise BudgetExceeded(f&quot;credit cap reached: {self.spent}/{self.max_credits}&quot;)
        if self.attempts &gt;= self.max_attempts:
            raise BudgetExceeded(f&quot;attempt cap reached: {self.attempts}/{self.max_attempts}&quot;)

        self.attempts += 1
        try:
            response = requests.request(
                method,
                url,
                params=params,
                headers={&quot;X-API-Key&quot;: os.environ[&quot;API_KEY&quot;]},
                timeout=30,
            )
        except requests.RequestException as error:
            raise BudgetExceeded(f&quot;request failed, stopping the run: {error}&quot;) from error
        if response.status_code == 402:
            raise BudgetExceeded(&quot;account is out of credits&quot;)
        if response.ok:
            self.spent += self.credits_per_call
        return response
</code></pre>
<p>Use one <code>CallBudget</code> per run and hand its <code>call</code> method to the agent as the only way to reach the API.</p>
<pre><code class="language-python">budget = CallBudget(max_credits=20, max_attempts=30)
url = os.environ[&quot;ENDPOINT_URL&quot;]  # copied from the endpoint&#39;s page

try:
    response = budget.call(url, params={&quot;q&quot;: &quot;example&quot;})
    print(response.status_code, f&quot;{budget.spent} credits spent&quot;)
except BudgetExceeded as stop:
    print(&quot;stopping the run:&quot;, stop)
</code></pre>
<p>A timeout or connection error counts as an attempt and ends the run cleanly instead of crashing the loop. The credit count is a local estimate: it adds the endpoint&#39;s listed cost for each successful response and doesn&#39;t read anything from the response. Treat it as a ceiling check, and compare it with the <code>X-Credits-Remaining</code> header, which every successful response carries with your balance after the call.</p>
<p>Set <code>max_attempts</code> above <code>max_credits</code> so a few free failures can retry, and keep the gap small. Here 20 credits allow 20 successful calls, and the 10 extra attempts allow up to 10 failures before the run stops. See also the <a href="https://perceptlayer.com/docs#requests">requests docs</a>.</p>
<h2 id="what-changes-when-the-agent-calls-through-mcp">What changes when the agent calls through MCP?</h2>
<p>The MCP server at <code>https://api.perceptlayer.com/mcp</code> exposes three tools, <code>search_endpoints</code>, <code>get_endpoint</code> and <code>call_endpoint</code>. <code>search_endpoints</code> and <code>get_endpoint</code> only look things up in the catalogue and cost nothing, so <code>call_endpoint</code> is the one tool that needs a budget check. A successful <code>call_endpoint</code> costs the credits shown on its endpoint, failed calls cost nothing, and its result reports the credits used and the balance remaining. The <a href="https://perceptlayer.com/docs#mcp">MCP docs</a> cover the tools.</p>
<p>The cap needs your own agent loop, because the loop is where tool calls get executed. Route every <code>call_endpoint</code> the model requests through a budget check, and let the loop stop when it raises. The sketch below assumes your MCP client gives you a function <code>send_call_endpoint(arguments)</code> that runs the tool and returns its result; <code>credits_per_call</code> is the cost listed on the endpoint.</p>
<pre><code class="language-python">class McpBudget:
    def __init__(self, max_credits, credits_per_call=1):
        self.max_credits = max_credits
        self.credits_per_call = credits_per_call
        self.spent = 0

    def call_endpoint(self, send_call_endpoint, arguments):
        if self.spent + self.credits_per_call &gt; self.max_credits:
            raise BudgetExceeded(f&quot;credit cap reached: {self.spent}/{self.max_credits}&quot;)
        try:
            result = send_call_endpoint(arguments)
        except Exception as error:
            raise BudgetExceeded(f&quot;tool call failed, stopping the run: {error}&quot;) from error
        if not getattr(result, &quot;isError&quot;, False):
            self.spent += self.credits_per_call
        return result
</code></pre>
<p>MCP marks a failed tool call with <code>isError</code> in its result (see the <a href="https://modelcontextprotocol.io/specification/2025-06-18/server/tools">MCP tools specification</a>), which is what the wrapper checks before counting credits. In the loop, when the model asks for <code>call_endpoint</code>, call <code>budget.call_endpoint(...)</code> instead of the client directly. Catch <code>BudgetExceeded</code> outside the loop and return what the agent has so far. As with the HTTP wrapper, the credit count is a local estimate, not a figure read from the response.</p>
<p>The related walkthrough, <a href="https://perceptlayer.com/blog/claude-agent-live-search-mcp">Claude MCP Web Search: Live Results for an Agent</a>, shows the tools in use.</p>
<h2 id="how-should-you-size-a-balance-for-an-agent">How should you size a balance for an agent?</h2>
<p>Size the balance for the capped worst case, then watch the typical case. Take runs per month × cap × credits per call. For 500 runs capped at 20 calls, that&#39;s 10,000 credits, which the Starter pack&#39;s 12,500 credits cover for $10. For 5,000 runs, it&#39;s 100,000 credits at the cap and 40,000 at 8 calls per run. One Growth pack (83,000 credits, $50) covers the typical 40,000. Two Growth packs (166,000 credits, $100) cover the 100,000-credit worst case. A Scale pack ($200) holds 400,000 credits at a lower rate per call, so it only pays off if you&#39;ll use most of it within the 12 months credits last.</p>
<p>Start on the free tier. Every month after you verify an email address you get 1,000 free credits, with no credit card, which is enough to measure your real calls per task with a cap in place.</p>
<p>The <a href="https://perceptlayer.com/docs#credits">credits docs</a> and the <a href="https://perceptlayer.com/pricing">pricing page</a> have the details and the packs. <a href="https://perceptlayer.com/signup">Get a free API key</a>, add the wrapper above, and run 20 tasks before you decide on a pack. More agent guides are in the <a href="https://perceptlayer.com/blog/category/ai-agents">AI agents category</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Claude MCP Web Search: Live Results for an Agent</title>
      <link>https://perceptlayer.com/blog/claude-agent-live-search-mcp</link>
      <guid isPermaLink="true">https://perceptlayer.com/blog/claude-agent-live-search-mcp</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
      <description>Add the Perceptlayer MCP server to Claude, let the agent call the Google Search endpoint, and count its cost: 1 credit per search call.</description>
      <category>AI agents</category>
      <category>claude</category>
      <category>mcp</category>
      <category>google search</category>
      <category>ai agents</category>
      <category>tool calling</category>
      <content:encoded><![CDATA[<h2 id="what-do-you-need-before-you-start">What do you need before you start?</h2>
<p>You need a Perceptlayer API key, the <code>claude</code> CLI or the Claude Agent SDK for Python, and an environment variable holding the key. Keys start with <code>sk_live_</code>. Every account gets 1,000 free credits each month after verifying an email address, with no credit card, so you can run everything below without paying. <a href="https://perceptlayer.com/signup">Get a free API key</a> first, then export it.</p>
<pre><code class="language-bash">export API_KEY=&quot;sk_live_...&quot;
</code></pre>
<p>The same key authenticates plain HTTP calls with the <code>X-API-Key</code> header and the MCP server with an <code>Authorization: Bearer</code> header. The <a href="https://perceptlayer.com/docs#authentication">authentication docs</a> cover both, plus the <code>api_key</code> query parameter for cases where headers aren&#39;t possible.</p>
<h2 id="how-does-a-claude-agent-search-the-web-over-mcp">How does a Claude agent search the web over MCP?</h2>
<p>The Model Context Protocol lets an agent discover and call tools on a remote server. Perceptlayer&#39;s server speaks <a href="https://modelcontextprotocol.io/specification/2025-06-18/basic/transports">Streamable HTTP</a>, the protocol&#39;s standard transport for remote servers, and gives the agent access to a catalogue of 248 websites and 1,659 endpoints, all returning JSON.</p>
<p>The server&#39;s tools are <code>search_endpoints</code>, <code>get_endpoint</code> and <code>call_endpoint</code>. The <a href="https://perceptlayer.com/docs#mcp">MCP section of the docs</a> describes the server, and the <a href="https://modelcontextprotocol.io/">protocol site</a> describes MCP itself.</p>
<h2 id="step-1-how-do-you-add-the-mcp-server-to-claude-code">Step 1: How do you add the MCP server to Claude Code?</h2>
<p>Run one command. It registers the server under the name <code>perceptlayer</code>, using the HTTP transport and your key as a Bearer token. Claude Code stores the entry in its MCP configuration.</p>
<pre><code class="language-bash">claude mcp add --transport http perceptlayer https://api.perceptlayer.com/mcp \
  --header &quot;Authorization: Bearer $API_KEY&quot;
</code></pre>
<p>Confirm the server connected:</p>
<pre><code class="language-bash">claude mcp list
</code></pre>
<p>You should see <code>perceptlayer</code> in the list. Inside a Claude Code session, the <code>/mcp</code> command shows the connection status of the server. If you want the entry scoped to a project instead of your user, the <a href="https://code.claude.com/docs/en/mcp">Claude Code MCP documentation</a> lists the scope options.</p>
<h3 id="ask-a-question-that-needs-live-search">Ask a question that needs live search</h3>
<p>Start <code>claude</code> and ask something the model can&#39;t know from training, such as: &quot;Use Perceptlayer to search Google for the latest Claude Code release notes and list the top five result titles with their URLs.&quot;</p>
<p>If Claude Code prompts you about the MCP tools, allow <code>search_endpoints</code>, <code>get_endpoint</code> and <code>call_endpoint</code> so the agent can reach the Google Search endpoint.</p>
<h2 id="step-2-which-google-search-endpoint-should-the-agent-use">Step 2: Which Google Search endpoint should the agent use?</h2>
<p>The Google Search API has 7 endpoints, and every one costs 1 credit per successful call. For an agent answering open questions, start with <code>/v1/google-search/search</code>, which takes a required <code>query</code> and optional parameters for country (<code>gl</code>), language (<code>hl</code>), result count and paging. The others fit narrower jobs, so tell the agent which to prefer if you care.</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>Required</th>
<th>Use it when</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://perceptlayer.com/apis/google-search/search"><code>/v1/google-search/search</code></a></td>
<td><code>query</code></td>
<td>General web search with the full parameter set</td>
</tr>
<tr>
<td><a href="https://perceptlayer.com/apis/google-search/search-light"><code>/v1/google-search/search-light</code></a></td>
<td><code>query</code></td>
<td>A smaller parameter set, with <code>num</code> for result count</td>
</tr>
<tr>
<td><a href="https://perceptlayer.com/apis/google-search/search-ai-overview"><code>/v1/google-search/search-ai-overview</code></a></td>
<td><code>query</code></td>
<td>You want the AI Overview for a query</td>
</tr>
<tr>
<td><a href="https://perceptlayer.com/apis/google-search/google-ai-mode-search"><code>/v1/google-search/google-ai-mode-search</code></a></td>
<td><code>q</code></td>
<td>You want an AI Mode answer, with <code>follow_up_token</code> for follow-ups</td>
</tr>
<tr>
<td><a href="https://perceptlayer.com/apis/google-search/search-light-autocomplete"><code>/v1/google-search/search-light-autocomplete</code></a></td>
<td><code>query</code></td>
<td>Query suggestions, with <code>hl</code> and <code>limit</code></td>
</tr>
</tbody></table>
<p>Note that <code>google-ai-mode-search</code> uses <code>q</code> where the others use <code>query</code>. A hard-coded script needs to get that right. The <a href="https://perceptlayer.com/apis/google-search">Google Search API page</a> lists all seven, including <a href="https://perceptlayer.com/apis/google-search/search-advanced"><code>search-advanced</code></a> and <a href="https://perceptlayer.com/apis/google-search/search-advanced-place"><code>search-advanced-place</code></a>, and the <a href="https://perceptlayer.com/verticals/search">search engines hub</a> shows related sources.</p>
<h2 id="step-3-how-do-you-check-the-endpoint-outside-the-agent">Step 3: How do you check the endpoint outside the agent?</h2>
<p>Call the endpoint directly with cURL before trusting an agent with it. If the request works from your terminal, any later failure is in the agent&#39;s prompt or tool use, not in the data call. The <code>-i</code> flag prints the response headers so you can see the status.</p>
<pre><code class="language-bash">curl -i &quot;https://api.perceptlayer.com/v1/google-search/search?query=claude%20code%20release%20notes&quot; \
  -H &quot;X-API-Key: $API_KEY&quot;
</code></pre>
<p>The endpoint returns the search results as JSON. To see a real response for your own query before you wire up the agent, run it in the playground on the <a href="https://perceptlayer.com/apis/google-search">Google Search API page</a>, which calls the endpoint with your key and shows the result; the <a href="https://perceptlayer.com/apis/google-search/search">Search endpoint page</a> lists every parameter.</p>
<p>Break the call on purpose to see the failure shape. Send a bad key and you get HTTP 401 with a body like <code>{&quot;error&quot;: {&quot;code&quot;: &quot;invalid_api_key&quot;, ...}}</code>, and the response carries <code>X-Credits-Used: 0</code>. The <a href="https://perceptlayer.com/docs#errors">errors section of the docs</a> lists every code.</p>
<h2 id="step-4-how-do-you-build-a-python-agent-that-uses-the-server">Step 4: How do you build a Python agent that uses the server?</h2>
<p>Use the Claude Agent SDK, point <code>mcp_servers</code> at the Perceptlayer URL, and allow the three tools by name. The script below asks one question, prints the answer, and counts how many times the agent called <code>call_endpoint</code>. The script counts every <code>call_endpoint</code> call and assumes 1 credit each, which holds for every Google Search endpoint and for nearly all others. It doesn&#39;t count <code>search_endpoints</code> or <code>get_endpoint</code> calls, and it counts failed calls too, which are free. Treat the total as an estimate, not a bill.</p>
<p>Install the SDK with <code>pip install claude-agent-sdk</code>, then save this as <code>agent.py</code>:</p>
<pre><code class="language-python">import asyncio
import os

from claude_agent_sdk import (
    AssistantMessage,
    ClaudeAgentOptions,
    TextBlock,
    ToolUseBlock,
    query,
)

QUESTION = &quot;What changed in the latest Claude Code release? Cite the result URLs you used.&quot;

SYSTEM_PROMPT = (
    &quot;You answer questions with live web search. &quot;
    &quot;Use the perceptlayer MCP tools to find and call a Google Search endpoint. &quot;
    &quot;Make at most 3 search calls per question. Cite the URLs you relied on.&quot;
)

OPTIONS = ClaudeAgentOptions(
    system_prompt=SYSTEM_PROMPT,
    mcp_servers={
        &quot;perceptlayer&quot;: {
            &quot;type&quot;: &quot;http&quot;,
            &quot;url&quot;: &quot;https://api.perceptlayer.com/mcp&quot;,
            &quot;headers&quot;: {&quot;Authorization&quot;: f&quot;Bearer {os.environ[&#39;API_KEY&#39;]}&quot;},
        }
    },
    allowed_tools=[
        &quot;mcp__perceptlayer__search_endpoints&quot;,
        &quot;mcp__perceptlayer__get_endpoint&quot;,
        &quot;mcp__perceptlayer__call_endpoint&quot;,
    ],
)

CREDITS_PER_CALL = 1  # every Google Search endpoint costs 1 credit; failed calls cost 0


async def main():
    data_calls = 0
    async for message in query(prompt=QUESTION, options=OPTIONS):
        if not isinstance(message, AssistantMessage):
            continue
        for block in message.content:
            if isinstance(block, ToolUseBlock) and block.name.endswith(&quot;call_endpoint&quot;):
                data_calls += 1
            elif isinstance(block, TextBlock):
                print(block.text)
    print(f&quot;\ndata calls: {data_calls}, credits (estimate): {data_calls * CREDITS_PER_CALL}&quot;)


asyncio.run(main())
</code></pre>
<p>The <a href="https://code.claude.com/docs/en/agent-sdk/python">Agent SDK documentation</a> covers the options used above.</p>
<h2 id="step-5-what-does-the-script-print">Step 5: What does the script print?</h2>
<p>Run it with <code>python agent.py</code>. The agent&#39;s answer text prints as it arrives, followed by one summary line in this format, where each <code>&lt;n&gt;</code> is a number from your run:</p>
<pre><code class="language-text">data calls: &lt;n&gt;, credits (estimate): &lt;n&gt;
</code></pre>
<p>Compare that number with the cap in the system prompt. If the count is higher than the cap, tighten the prompt.</p>
<h3 id="why-cap-the-number-of-search-calls">Why cap the number of search calls?</h3>
<p>Each successful search call costs 1 credit, so the number of calls sets the credit cost of a question. The prompt line &quot;at most 3 search calls per question&quot; bounds that spend at 3 credits, and the counter shows whether the agent respects it. Treat the cap as a guardrail, not a guarantee, and check the printed count while you tune the prompt.</p>
<h2 id="what-does-each-question-cost">What does each question cost?</h2>
<p>A successful <code>call_endpoint</code> call to a Google Search endpoint costs 1 credit, so an agent that searches twice spends 2 credits. The figures below count only those search calls; they leave out <code>search_endpoints</code> and <code>get_endpoint</code> calls. The free tier of 1,000 credits a month covers 1,000 search calls, and purchased credits last 12 months with no subscription and no seats. The table below counts Perceptlayer credits only. Claude model tokens are billed separately by Anthropic, so the real cost of a question is higher than the credit figure.</p>
<table>
<thead>
<tr>
<th>Workload</th>
<th>Calls</th>
<th>Credits</th>
<th>Covered by</th>
</tr>
</thead>
<tbody><tr>
<td>300 questions × 1 search each</td>
<td>300</td>
<td>300</td>
<td>Free tier (1,000 credits)</td>
</tr>
<tr>
<td>300 questions × 3 searches each</td>
<td>900</td>
<td>900</td>
<td>Free tier (1,000 credits)</td>
</tr>
<tr>
<td>10,000 questions × 1 search each</td>
<td>10,000</td>
<td>10,000</td>
<td>Starter pack, 12,500 credits for $10</td>
</tr>
<tr>
<td>5,000 questions × 3 searches each</td>
<td>15,000</td>
<td>15,000</td>
<td>Growth pack, 83,000 credits for $50</td>
</tr>
</tbody></table>
<p>Packs are bought whole. The Starter pack is $10 for 12,500 credits, an effective $0.80 per 1,000 calls, and it covers the 10,000 calls in the third row. The Growth pack is $50 for 83,000 credits, an effective $0.60 per 1,000 calls, and it covers the 15,000 calls in the last row. The Scale pack is $200 for 400,000 credits, or $0.50 per 1,000 calls. The <a href="https://perceptlayer.com/pricing">pricing page</a> has all three, and the <a href="https://perceptlayer.com/docs#credits">credits section of the docs</a> explains how a call is billed.</p>
<p>Because failed calls are free, a rate-limited or erroring call doesn&#39;t add to the total. Only a successful response debits credits.</p>
<h2 id="what-happens-when-a-call-fails">What happens when a call fails?</h2>
<p>A failed call returns an HTTP status and a JSON body with <code>error.code</code>, <code>error.message</code> and <code>error.request_id</code>, and it costs nothing. Give the agent a rule for each case in the system prompt so it knows whether to retry, change the query, or stop, and doesn&#39;t loop.</p>
<table>
<thead>
<tr>
<th>Code</th>
<th>Status</th>
<th>What the agent should do</th>
</tr>
</thead>
<tbody><tr>
<td><code>invalid_parameters</code></td>
<td>422</td>
<td>Check the parameters against the endpoint page, fix, retry once</td>
</tr>
<tr>
<td><code>rate_limited</code></td>
<td>429</td>
<td>Wait and retry; see <a href="https://perceptlayer.com/docs#limits">rate limits</a></td>
</tr>
<tr>
<td><code>source_error</code></td>
<td>502</td>
<td>Retry once, then report that search is unavailable</td>
</tr>
<tr>
<td><code>temporarily_unavailable</code></td>
<td>503</td>
<td>Retry later</td>
</tr>
<tr>
<td><code>insufficient_credits</code></td>
<td>402</td>
<td>Stop and tell the user to top up</td>
</tr>
<tr>
<td><code>invalid_api_key</code></td>
<td>401</td>
<td>Stop; the key is wrong or revoked</td>
</tr>
<tr>
<td><code>restricted_endpoint</code></td>
<td>403</td>
<td>Stop; the endpoint needs the restricted-endpoint terms accepted in the dashboard</td>
</tr>
</tbody></table>
<p>Add a sentence such as &quot;If a tool call fails with insufficient_credits or invalid_api_key, stop and say so&quot; to <code>SYSTEM_PROMPT</code>. Retries on the transient codes are safe for your balance because failures aren&#39;t billed.</p>
<h2 id="next-step-point-the-same-server-at-more-sources">Next step: point the same server at more sources</h2>
<p>The server isn&#39;t limited to Google Search. The same key and the same three tools reach the rest of the catalogue, so the agent can move from a search result to a listing, a review page or a price without a new integration. Some endpoints, covering social, personal and SEO data, need you to accept the restricted-endpoint terms in the dashboard first.</p>
<p>Add the server with the command in step 1, ask your agent a question that needs live data, and check the credit count it prints. To read more about how agents pick and call endpoints, see the <a href="https://perceptlayer.com/docs#mcp">MCP docs</a>, and for a worked example of one data source end to end, read <a href="https://perceptlayer.com/blog/zillow-listings-api-json">how to get Zillow listings as JSON</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Zillow Listings API: Get Property Data as JSON</title>
      <link>https://perceptlayer.com/blog/zillow-listings-api-json</link>
      <guid isPermaLink="true">https://perceptlayer.com/blog/zillow-listings-api-json</guid>
      <pubDate>Thu, 22 Oct 2026 00:00:00 GMT</pubDate>
      <description>One GET to /v1/zillow/search returns listings near a point as JSON with price, beds, baths and address. Parameters, a real response, 1 credit per call.</description>
      <category>API guides</category>
      <category>zillow</category>
      <category>real estate</category>
      <category>listings</category>
      <category>property data</category>
      <category>api</category>
      <content:encoded><![CDATA[<h2 id="is-there-an-official-zillow-listings-api">Is there an official Zillow listings API?</h2>
<p>Zillow Group runs a <a href="https://www.zillowgroup.com/developers/">developer site</a> that lists APIs for MLS listings, rentals feeds, public data and Zestimates, offered to the public and to partners. Those programs have their own terms and eligibility. If your use fits one, check that route first, because it&#39;s the authoritative one.</p>
<p>If you aren&#39;t, or you need a plain search-by-location call returning JSON today, that&#39;s the gap the Perceptlayer <a href="https://perceptlayer.com/apis/zillow">Zillow API</a> fills. It exposes 10 endpoints under one key, with the same authentication and credit model as every site in the catalogue: Zillow is one of 248 websites and 1,659 endpoints in the catalogue.</p>
<p>Data is collected from publicly available sources and returned as found at the time of the call. Listing fields can change between calls, so store the timestamp of each pull.</p>
<h2 id="how-do-you-get-a-key-and-make-the-first-call">How do you get a key and make the first call?</h2>
<p>Sign up, verify your email and copy a key that starts with <code>sk_live_</code>. Send it in the <code>X-API-Key</code> header on every request. If you can&#39;t set headers, the <code>api_key</code> query parameter works instead. The free tier is 1,000 credits a month, enough for 1,000 Zillow calls, so you can run everything below without paying. The <a href="https://perceptlayer.com/docs#authentication">authentication docs</a> cover both options.</p>
<p><a href="https://perceptlayer.com/signup">Get a free API key</a>, export it, then run this search for listings within 5 miles of a point in Seattle (latitude 47.6062, longitude -122.3321):</p>
<pre><code class="language-bash">export API_KEY=sk_live_your_key_here

curl &quot;https://api.perceptlayer.com/v1/zillow/search?latitude=47.6062&amp;longitude=-122.3321&amp;radius_mi=5&amp;listing_category=category1&amp;page_size=20&quot; \
  -H &quot;X-API-Key: $API_KEY&quot;
</code></pre>
<p>The response is JSON with a <code>data</code> object. The listings sit in <code>data.listings</code>, one object per property. The <a href="https://perceptlayer.com/docs#requests">requests docs</a> explain the general conventions that apply to every endpoint.</p>
<h2 id="which-parameters-does-the-search-endpoint-take">Which parameters does the search endpoint take?</h2>
<p><code>/v1/zillow/search</code> has no required parameters, but you&#39;ll always want to say where to look. Use a centre point with <code>radius_mi</code>, a bounding box made of four coordinates, <code>region_ids[]</code>, or a <code>polygon</code> string. Everything else narrows the results by price, size, status or keyword. The table groups all 29 parameters by job.</p>
<table>
<thead>
<tr>
<th>Group</th>
<th>Parameter</th>
<th>Type</th>
</tr>
</thead>
<tbody><tr>
<td>Point and radius</td>
<td><code>latitude</code>, <code>longitude</code>, <code>radius_mi</code></td>
<td>number</td>
</tr>
<tr>
<td>Bounding box</td>
<td><code>north_latitude</code>, <code>south_latitude</code>, <code>east_longitude</code>, <code>west_longitude</code></td>
<td>number</td>
</tr>
<tr>
<td>Region</td>
<td><code>region_ids[]</code></td>
<td>list of integer</td>
</tr>
<tr>
<td>Region</td>
<td><code>region_id_type</code></td>
<td>integer</td>
</tr>
<tr>
<td>Shape</td>
<td><code>polygon</code></td>
<td>string</td>
</tr>
<tr>
<td>Category</td>
<td><code>listing_category</code></td>
<td>string: <code>all</code>, <code>category1</code>, <code>category2</code></td>
</tr>
<tr>
<td>Status</td>
<td><code>home_statuses[]</code></td>
<td>list of string</td>
</tr>
<tr>
<td>Status</td>
<td><code>include_off_market</code>, <code>allow_empty</code></td>
<td>boolean</td>
</tr>
<tr>
<td>Price</td>
<td><code>min_price</code>, <code>max_price</code></td>
<td>integer</td>
</tr>
<tr>
<td>Size</td>
<td><code>min_beds</code>, <code>max_beds</code>, <code>min_baths</code>, <code>max_baths</code></td>
<td>integer</td>
</tr>
<tr>
<td>Size</td>
<td><code>living_area_min</code>, <code>lot_size_min</code>, <code>year_built_min</code></td>
<td>integer</td>
</tr>
<tr>
<td>Text</td>
<td><code>keywords</code></td>
<td>string</td>
</tr>
<tr>
<td>Map</td>
<td><code>zoom_level</code></td>
<td>integer</td>
</tr>
<tr>
<td>Sorting</td>
<td><code>sort_order</code></td>
<td>string, see below</td>
</tr>
<tr>
<td>Sorting</td>
<td><code>sort_ascending</code></td>
<td>boolean</td>
</tr>
<tr>
<td>Paging</td>
<td><code>page</code>, <code>page_size</code></td>
<td>integer</td>
</tr>
</tbody></table>
<p><code>sort_order</code> accepts one of <code>baths</code>, <code>yearBuilt</code>, <code>price</code>, <code>relevance</code>, <code>beds</code>, <code>daysOn</code>, <code>featured</code>, <code>livingArea</code>, <code>lotArea</code>, <code>nearby</code>, <code>recentlyChanged</code>, <code>rentalDays</code> or <code>rentalPriorityScore</code>.</p>
<p>The reference doesn&#39;t say what <code>category1</code> and <code>category2</code> select, so try each on your area and compare the results before you build on one. The example request above uses <code>category1</code>.</p>
<h3 id="narrowing-the-search">Narrowing the search</h3>
<p>Add filters as plain query parameters. This request asks for homes with at least 3 bedrooms and 2 bathrooms between $500,000 and $900,000, sorted by price:</p>
<pre><code class="language-bash">curl &quot;https://api.perceptlayer.com/v1/zillow/search?latitude=47.6062&amp;longitude=-122.3321&amp;radius_mi=5&amp;min_price=500000&amp;max_price=900000&amp;min_beds=3&amp;min_baths=2&amp;sort_order=price&amp;sort_ascending=true&amp;page_size=20&quot; \
  -H &quot;X-API-Key: $API_KEY&quot;
</code></pre>
<p>A tight filter can return few or no listings, so loosen one filter at a time if a search comes back thin. The search endpoint also takes an <code>allow_empty</code> boolean.</p>
<h2 id="what-does-a-search-response-look-like">What does a search response look like?</h2>
<p>The response wraps results in <code>data.listings</code>. Each listing carries a <code>zpid</code> (the property ID), a <code>location</code>, an <code>address</code>, a <code>media</code> block with photo links, and the facts a valuation or lead tool needs: <code>bedrooms</code>, <code>bathrooms</code>, <code>livingArea</code>, <code>yearBuilt</code>, <code>propertyType</code>, a <code>listing</code> status object and a <code>price</code> object. This is a trimmed excerpt of a recorded response to the Seattle request above:</p>
<pre><code class="language-json">{
  &quot;data&quot;: {
    &quot;listings&quot;: [
      {
        &quot;zpid&quot;: 2068074790,
        &quot;location&quot;: {
          &quot;latitude&quot;: 47.592564,
          &quot;longitude&quot;: -122.386925
        },
        &quot;address&quot;: {
          &quot;streetAddress&quot;: &quot;1124 Sunset Avenue SW&quot;,
          &quot;zipcode&quot;: &quot;98116&quot;,
          &quot;city&quot;: &quot;Seattle&quot;,
          &quot;state&quot;: &quot;WA&quot;
        },
        &quot;media&quot;: { … },
        &quot;currency&quot;: &quot;usd&quot;,
        &quot;country&quot;: &quot;usa&quot;,
        &quot;bestGuessTimeZone&quot;: &quot;America/Los_Angeles&quot;,
        &quot;bathrooms&quot;: 6,
        &quot;bedrooms&quot;: 4,
        &quot;livingArea&quot;: 7870,
        &quot;yearBuilt&quot;: 2015,
        &quot;lotSizeWithUnit&quot;: {
          &quot;lotSize&quot;: 0.2652,
          &quot;lotSizeUnit&quot;: &quot;acres&quot;
        },
        &quot;propertyType&quot;: &quot;singleFamily&quot;,
        &quot;listing&quot;: {
          &quot;listingStatus&quot;: &quot;forSale&quot;,
          &quot;marketingStatus&quot;: &quot;active&quot;,
          &quot;palsId&quot;: &quot;39001_2591845&quot;
        },
        &quot;price&quot;: {
          &quot;value&quot;: 14000000,
          &quot;pricePerSquareFoot&quot;: 1779
        …
</code></pre>
<p>Field names are camelCase and nested. Price is at <code>price.value</code>, the street at <code>address.streetAddress</code>, and the lot size is an object with a value and a unit. The excerpt also holds media flags and recommendation blocks that we&#39;ve cut with <code>…</code>. Treat any field you don&#39;t see in your own response as absent, and check for it in code rather than assuming it&#39;s there.</p>
<h2 id="how-do-you-fetch-one-property-by-id">How do you fetch one property by ID?</h2>
<p>Take the <code>zpid</code> from a search result and pass it to <code>/v1/zillow/property</code>. The endpoint has no required parameters and accepts a <code>zpid[]</code> list. Per-endpoint pages, such as <a href="https://perceptlayer.com/apis/zillow/property">Zillow API: Property</a>, list the parameters and let you try the call.</p>
<pre><code class="language-bash">curl &quot;https://api.perceptlayer.com/v1/zillow/property?zpid[]=2068074790&quot; \
  -H &quot;X-API-Key: $API_KEY&quot;
</code></pre>
<p>We haven&#39;t recorded a property response for this guide, so we won&#39;t show one. Run the call on your own key and inspect the JSON before you map fields.</p>
<p>The other Zillow endpoints follow the same pattern:</p>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>Use it for</th>
<th>Credits</th>
</tr>
</thead>
<tbody><tr>
<td><code>/v1/zillow/autocomplete</code></td>
<td>Turn a typed place name into suggestions (<code>query</code> required)</td>
<td>1</td>
</tr>
<tr>
<td><code>/v1/zillow/photos</code></td>
<td>Photos for one or more <code>zpid[]</code></td>
<td>1</td>
</tr>
<tr>
<td><code>/v1/zillow/photo</code></td>
<td>One photo by <code>photo_id</code>, with optional <code>size</code></td>
<td>1</td>
</tr>
<tr>
<td><code>/v1/zillow/building</code></td>
<td>A building by <code>slug</code>, <code>lot_id</code> or coordinates</td>
<td>1</td>
</tr>
<tr>
<td><code>/v1/zillow/schools</code></td>
<td>Schools by ID or bounding box</td>
<td>1</td>
</tr>
<tr>
<td><code>/v1/zillow/market-data</code></td>
<td>Market statistics (<code>family</code> and <code>geography</code> required)</td>
<td>1</td>
</tr>
</tbody></table>
<h2 id="how-do-you-call-it-from-python">How do you call it from Python?</h2>
<p>The same request in Python needs only <code>requests</code>. This script searches around the same Seattle point, then prints address, bedrooms and price for each listing. It reads the key from the environment and checks the status before parsing, so a failed call raises an error instead of returning bad data.</p>
<pre><code class="language-python">import os
import requests

resp = requests.get(
    &quot;https://api.perceptlayer.com/v1/zillow/search&quot;,
    headers={&quot;X-API-Key&quot;: os.environ[&quot;API_KEY&quot;]},
    params={
        &quot;latitude&quot;: 47.6062,
        &quot;longitude&quot;: -122.3321,
        &quot;radius_mi&quot;: 5,
        &quot;listing_category&quot;: &quot;category1&quot;,
        &quot;page_size&quot;: 20,
    },
    timeout=30,
)
resp.raise_for_status()

for item in resp.json()[&quot;data&quot;][&quot;listings&quot;]:
    print(
        item.get(&quot;zpid&quot;),
        (item.get(&quot;address&quot;) or {}).get(&quot;streetAddress&quot;),
        item.get(&quot;bedrooms&quot;),
        (item.get(&quot;price&quot;) or {}).get(&quot;value&quot;),
    )
</code></pre>
<p>To page through more results, increase <code>page</code> and keep <code>page_size</code> fixed. Each page is its own call and its own credit, so pick the smallest <code>page_size</code> that covers your need.</p>
<h2 id="what-does-it-cost-per-1-000-calls">What does it cost per 1,000 calls?</h2>
<p>Every Zillow endpoint costs 1 credit per successful call, so 1,000 search calls use 1,000 credits. Failed calls cost nothing and come back with <code>X-Credits-Used: 0</code>. The free tier of 1,000 credits a month covers 1,000 calls. Beyond that, purchased credits last 12 months, with no subscription and no seats. The <a href="https://perceptlayer.com/docs#credits">credits docs</a> and <a href="https://perceptlayer.com/pricing">pricing page</a> have the detail.</p>
<table>
<thead>
<tr>
<th>Plan</th>
<th>Price</th>
<th>Credits</th>
<th>Cost per 1,000 calls</th>
</tr>
</thead>
<tbody><tr>
<td>Free tier</td>
<td>$0</td>
<td>1,000 a month</td>
<td>$0</td>
</tr>
<tr>
<td>Starter</td>
<td>$10</td>
<td>12,500</td>
<td>$0.80</td>
</tr>
<tr>
<td>Growth</td>
<td>$50</td>
<td>83,000</td>
<td>$0.60</td>
</tr>
<tr>
<td>Scale</td>
<td>$200</td>
<td>400,000</td>
<td>$0.50</td>
</tr>
</tbody></table>
<p>A worked example: refreshing 10 search areas twice a day for 30 days is 10 × 2 × 30 = 600 calls, or 600 credits, which fits inside the free tier. A job making 10,000 calls a month uses 10,000 credits, within the Starter pack&#39;s 12,500, at $0.80 per 1,000 calls.</p>
<h2 id="what-happens-when-a-call-fails">What happens when a call fails?</h2>
<p>A failed call returns an HTTP status and a JSON body shaped as <code>{&quot;error&quot;: {&quot;code&quot;, &quot;message&quot;, &quot;request_id&quot;}}</code>, and it isn&#39;t billed. The codes you&#39;ll most likely meet are <code>invalid_api_key</code> (401), <code>insufficient_credits</code> (402), <code>invalid_parameters</code> (422), <code>rate_limited</code> (429) and <code>source_error</code> (502). The <a href="https://perceptlayer.com/docs#errors">errors docs</a> list all of them.</p>
<p>Handle them by code, not by message text. Retry <code>rate_limited</code>, <code>source_error</code> and <code>temporarily_unavailable</code> (503) with a growing delay between attempts. A 429 is the standard &quot;too many requests&quot; status, defined in <a href="https://www.rfc-editor.org/rfc/rfc6585#section-4">RFC 6585</a> (2012). Fix <code>invalid_parameters</code> before you retry, since repeating the same request will fail the same way. See the <a href="https://perceptlayer.com/docs#limits">rate limits docs</a> for the limits on your key.</p>
<h2 id="can-an-ai-agent-call-these-endpoints">Can an AI agent call these endpoints?</h2>
<p>Yes. The MCP server at <code>https://api.perceptlayer.com/mcp</code> exposes <code>search_endpoints</code>, <code>get_endpoint</code> and <code>call_endpoint</code>, so an agent can find the Zillow search endpoint and call it without custom glue code. In Claude Code, add it with the command below, then ask for listings in a city. The <a href="https://perceptlayer.com/docs#mcp">MCP docs</a> cover other clients.</p>
<pre><code class="language-bash">claude mcp add --transport http perceptlayer https://api.perceptlayer.com/mcp --header &quot;Authorization: Bearer $API_KEY&quot;
</code></pre>
<p>Only successful calls are billed and failed calls cost nothing, so cap the number of calls a task may make. For more real estate sources, see the <a href="https://perceptlayer.com/verticals/real-estate">real estate APIs</a>.</p>
<p>Ready to try it? <a href="https://perceptlayer.com/signup">Get a free API key</a>, run the Seattle search above, and check the <a href="https://perceptlayer.com/apis/zillow">Zillow API page</a> for the other nine endpoints.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
