Call APIVerve APIs as tools from your own code

The MCP server hands an assistant every tool at once. When you own the agent, you write the loop yourself, and that is where you decide which tools exist, what they cost and what happens when one fails.

The earlier courses in this certification connected agents to APIVerve over MCP: an assistant discovers the tools, calls them, and you watch. That is the right shape for a person chatting with a model. It is the wrong shape for a backend job that runs at 3am with nobody watching, because the client decides how the loop runs, and you want to decide.

So this course drops one level. You will call a model's API directly, hand it a short list of tools built from APIVerve endpoints, and run the tool-use loop in your own server code. The model never sees your API key and never makes an HTTP request. It asks for a call; your code decides whether to make it, makes it, and hands back the result.

By the end of this lesson you will have three small files you reuse for the rest of the course: a catalog of tool definitions, an executor that turns a tool call into a REST request, and a loop that drives a model until it has an answer.

What a tool call actually is

Strip away the vocabulary and tool use is a structured conversation with four moves:

  1. You send the model a prompt plus a list of tools. Each tool is a name, a description and a JSON Schema for its input.
  2. The model replies either with text (it is done) or with one or more tool calls: a tool name plus an input object that should match the schema.
  3. Your code runs each call and sends the results back, tagged with the id of the call they answer.
  4. Repeat until the model replies with text, or until you stop it.

Every vendor that supports tool calling uses this shape. The field names differ (input_schema here, parameters there; tool_use blocks in one API, tool_calls in another), but the loop is identical. That is why the code below keeps everything except one function vendor-neutral: the catalog, the schema builder and the executor know nothing about which model you use. Only the loop speaks one vendor's wire format, and swapping vendors means rewriting about forty lines.

The examples use Anthropic's Messages API through its official SDK, because its request and response shapes are documented and stable. If you use a different provider, keep everything else and port runAgent.

Tool definitions come from the API's own parameters

You could write tool schemas by hand. Don't. Every APIVerve endpoint already publishes its parameters, with names, types, descriptions, examples and which ones are required, on its reference page. Copy them into a small catalog and generate schemas from it, and your tool definitions cannot drift from the API's actual contract.

Start with the APIs the triage agent in lesson 3 will need. Look at what one of them returns:

Try it liveEmail Validator20 credits
GEThttps://api.apiverve.com/v1/emailvalidator
curl "https://api.apiverve.com/v1/emailvalidator?email=support%40myspace.com" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

isRoleAccount is true and isCompanyEmail is true: this is a shared company mailbox, not a person.

Try it liveIP Lookup10 credits
GEThttps://api.apiverve.com/v1/iplookup
curl "https://api.apiverve.com/v1/iplookup?ip=173.172.81.20" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

country, regionName and timezone are on every plan. city, coordinates and asn are premium.

Try it liveVPN Proxy Detector10 credits
GEThttps://api.apiverve.com/v1/vpndetector
curl "https://api.apiverve.com/v1/vpndetector?ip=103.62.49.210" \
  -H "x-api-key: YOUR_API_KEY"
Sign in or create a free account to run this with your own key.

is_vpn and is_datacenter are on every plan. is_tor and risk_level are premium.

Now write the catalog. This is a trimmed copy of each endpoint's published definition: the same field names, the same structure, only the parts a tool needs.

catalog.json
[
  {
    "id": "emailvalidator",
    "title": "Email Validator",
    "description": "Checks whether an email address is well-formed, whether its domain can receive mail, and whether it is disposable, a role account or a free provider. Returns a normalized address for spotting duplicates.",
    "tokenUse": 20,
    "endpoints": [{
      "method": "GET",
      "parameters": [
        { "name": "email", "type": "string", "description": "The email address to validate", "example": "[email protected]", "required": true }
      ]
    }]
  },
  {
    "id": "iplookup",
    "title": "IP Lookup",
    "description": "Resolves an IP address to its country, region, timezone and continent.",
    "tokenUse": 10,
    "endpoints": [{
      "method": "GET",
      "parameters": [
        { "name": "ip", "type": "string", "description": "The IP address to locate", "example": "173.172.81.20", "required": true }
      ]
    }]
  },
  {
    "id": "vpndetector",
    "title": "VPN Proxy Detector",
    "description": "Checks whether an IP address belongs to a VPN or a cloud datacenter.",
    "tokenUse": 10,
    "endpoints": [{
      "method": "GET",
      "parameters": [
        { "name": "ip", "type": "string", "description": "The IP address to check", "example": "103.62.49.210", "required": true }
      ]
    }]
  }
]

Three things in that file do real work later:

  • tokenUse is the credit cost per call. You will put it in the tool description so the model can see the price, which is exactly what the APIVerve MCP server does, and in lesson 4 you will use it to enforce a per-run budget.
  • method tells the executor whether to send a query string or a JSON body. emailvalidator is a GET; the page and text tools in the next lesson are POST.
  • premium, when a parameter has it, marks an input only some plans accept. The schema builder leaves premium parameters out by default, so the model cannot ask for something your key will not honour.

Build the schemas and the executor

Set up a project. Both languages read the same catalog.json.

mkdir agent-workflows && cd agent-workflows
npm init -y && npm pkg set type=module
npm install @anthropic-ai/sdk
# or, for Python
pip install anthropic requests

export APIVERVE_API_KEY=YOUR_API_KEY
export ANTHROPIC_API_KEY=YOUR_MODEL_PROVIDER_KEY

The tools module has three jobs: build a vendor-neutral tool spec from a definition, run one REST call, and wrap that call so that every outcome, success or failure, becomes something the model can read.

import { readFileSync } from 'node:fs';

const BASE = 'https://api.apiverve.com/v1';
const CATALOG = JSON.parse(readFileSync(new URL('./catalog.json', import.meta.url), 'utf8'));
export const DEFS = new Map(CATALOG.map((d) => [d.id, d]));

// Vendor-neutral: { name, description, schema }
export function toolFromDefinition(def, { includePremium = false } = {}) {
  const properties = {};
  const required = [];
  for (const p of def.endpoints[0].parameters) {
    if (p.premium && !includePremium) continue;
    const example = p.example !== undefined ? ` Example: ${p.example}.` : '';
    properties[p.name] = { type: p.type, description: `${p.description}.${example}` };
    if (p.required) required.push(p.name);
  }
  return {
    name: def.id,
    description: `${def.title}: ${def.description} Costs ${def.tokenUse} credits per call.`,
    schema: { type: 'object', properties, required, additionalProperties: false },
  };
}

export const apiTools = (ids) => ids.map((id) => toolFromDefinition(DEFS.get(id)));

export async function callApiverve(def, input, { timeoutMs = 30000 } = {}) {
  const key = process.env.APIVERVE_API_KEY;
  if (!key) throw new Error('APIVERVE_API_KEY is not set');
  const ep = def.endpoints[0];
  const allowed = new Set(ep.parameters.map((p) => p.name));
  const params = Object.fromEntries(Object.entries(input ?? {}).filter(([k]) => allowed.has(k)));

  let url = `${BASE}/${def.id}`;
  const init = { method: ep.method, headers: { 'x-api-key': key }, signal: AbortSignal.timeout(timeoutMs) };
  if (ep.method === 'GET') {
    url += `?${new URLSearchParams(params)}`;
  } else {
    init.headers['Content-Type'] = 'application/json';
    init.body = JSON.stringify(params);
  }

  const res = await fetch(url, init);
  const body = await res.json().catch(() => null);
  if (!res.ok || body?.status !== 'ok') {
    const outOfCredits = res.status === 429 && res.headers.get('x-api-remaining-credits') === '0';
    return { ok: false, status: res.status, outOfCredits, error: body?.error || `HTTP ${res.status}` };
  }
  return { ok: true, data: body.data };
}

const MAX_RESULT_CHARS = 8000;

// Every outcome becomes { content, isError, fatal? } so the loop never throws mid-turn.
export async function executeApiTool(name, input) {
  const def = DEFS.get(name);
  if (!def) return { content: `Unknown tool "${name}".`, isError: true };
  try {
    const r = await callApiverve(def, input);
    if (r.ok) {
      let text = JSON.stringify(r.data);
      if (text.length > MAX_RESULT_CHARS) text = `${text.slice(0, MAX_RESULT_CHARS)} [truncated]`;
      return { content: text, isError: false };
    }
    const fatal = r.status === 401 || r.status === 403 || r.outOfCredits;
    return { content: `${name} failed (${r.status}): ${r.error}`, isError: true, fatal };
  } catch (e) {
    return { content: `${name} failed: ${e.name === 'TimeoutError' ? 'timed out' : e.message}`, isError: true };
  }
}

Read the executor's error handling closely, because it is the part people get wrong.

Failures go back to the model as results, not exceptions. A tool call the model made is a question it is waiting on. If you throw, the whole run dies over something the model might have handled: a typo in an IP address comes back as a 4xx with a readable message, and the model can fix the argument and try again. So a failed call becomes a result flagged isError, and the conversation continues.

Some failures are fatal, and those stop the run. A 401 means the key is missing or unrecognised. A 403 means the key is valid but not allowed this call, from key scoping, an IP allow-list or revocation. A 429 with x-api-remaining-credits: 0 means the month's credits are gone. None of these can be fixed by the model choosing different arguments, and a model that sees "error" tends to retry. The fatal flag lets the loop stop instead of paying for more model turns that cannot succeed. A 429 with credits remaining is the per-minute rate limit, which is transient, so it stays non-fatal.

The executor filters arguments against the definition. APIVerve silently ignores parameters it does not recognise rather than rejecting them, so a model that invents "strict": true would get a normal response and a false belief that its extra argument did something. Dropping undeclared keys before the call keeps the request honest.

Results are capped. A tool result goes into the model's context on every later turn. A large response pasted in whole costs tokens each time and buries the fields that matter. Eight thousand characters is plenty for verification APIs; you will see a better pattern for large page content in the next lesson.

Check yourself

Your agent calls iplookup and gets a 403 with a JSON body. What should the executor do?

Answer the check above to continue.

Run every example on your own key. Free plan, no card.

Scaling up?

Volume pricing, custom SLAs, and dedicated support for high-traffic teams.

Contact sales