Build with AI

An agent can call every endpoint in this API. Quotes, collections, payouts and orchestrations are plain HTTP with one header.

Thus you connect Figo to an agent in two steps. First, describe the endpoints that the agent can use. Second, decide carefully which endpoints to keep away from it.

This page shows that work in the SDKs that people use. The Figo part is the same in all of them: a fetch to {BASE_URL}/v1 with your x-api-key. There is no Figo SDK to install.

Before you wire anything up

An agent that calls this API can move real money. Four rules make that safe. They are more important than any code on this page.

Build on sk_test_ first. Test and live keys take identical requests and return identical shapes, so nothing about your agent changes when you switch. Only sk_live_ moves money — give your agent a test key until you have watched it behave.

Keep read tools apart from money tools. An agent can safely call quotes, corridors, balance and transaction lookups at any time. It cannot safely call POST /v1/collections, /v1/payouts and /v1/orchestrate, because these three make a real obligation. Make them separate tools, and control them differently.

Quote, confirm, then execute. Each endpoint that moves money has a quote endpoint. A safe agent gets the quote first. It then shows the USD amount and the local amount to a person. It executes only after that person agrees.

Never put the key in the prompt. Your tool code holds the API key and adds the header. The model sees only the arguments of the tool and the result. It never sees the key. A model that cannot read the key cannot release it.

The tool surface

Give an agent only the tools that the job needs. For example, a support agent that answers the question "where is my payout?" needs the read column only.

Safe to call freely Creates an obligation
GET /v1/corridors — where you can move money POST /v1/collections — take money from a customer
GET /v1/balance — your USD balance POST /v1/payouts — send money to a recipient
POST /v1/collections/quote — price a collection POST /v1/orchestrate — any source to any destination
POST /v1/payouts/quote — price a payout POST /v1/virtual-accounts — issue an account
POST /v1/orchestrate/quote — price end-to-end POST /v1/customers — create a customer record
GET /v1/transactions, GET /v1/transactions/{id} POST /v1/payment-links — create a payment link
GET /v1/orchestrate/{id} — follow an orchestration
GET /v1/collections/assets, /v1/payouts/assets

A quote uses POST because it has a body. But a quote is a read. It gives the price of a movement, and it does not start the movement.

Claude

Make each endpoint a tool. The tool runner of the SDK then operates the loop.

import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";

const BASE_URL = "https://devapi.spendfigo.com";

// One helper, every tool. The key lives here and only here.
const figo = async (path: string, body?: unknown) => {
  const res = await fetch(`${BASE_URL}${path}`, {
    method: body ? "POST" : "GET",
    headers: {
      "x-api-key": process.env.FIGO_API_KEY!,
      "content-type": "application/json",
    },
    ...(body ? { body: JSON.stringify(body) } : {}),
  });
  return JSON.stringify(await res.json());
};

const quoteCollection = betaZodTool({
  name: "quote_collection",
  description:
    "Price a collection before creating one. Returns the local amount the " +
    "customer pays, the USD you would be credited, and the fees. Read-only — " +
    "no money moves. Call this before create_collection.",
  inputSchema: z.object({
    country: z.string().describe("Corridor country, ISO-3166 alpha-2, e.g. NG"),
    currency: z.string().describe("Local currency the customer pays, e.g. NGN"),
    amount: z.number().describe("Amount, interpreted per amount_type"),
    amount_type: z
      .enum(["source", "destination"])
      .optional()
      .describe("source = local the customer pays; destination = USD you want"),
  }),
  run: (input) => figo("/v1/collections/quote", input),
});

const runner = client.beta.messages.toolRunner({
  model: "claude-opus-5",
  max_tokens: 16000,
  tools: [quoteCollection /* , … */],
  messages: [
    { role: "user", content: "What would collecting ₦500,000 in Nigeria credit me?" },
  ],
});

const answer = await runner.until_done();

In the description, tell the model when to call the tool. Do not write only what the tool does. The model uses that sentence to decide.

The text "call this before create_collection" helps more than a long description of the response.

Gating the money

The tool runner calls your run function each time the model asks. For a tool that moves money, put a person inside that function. The model asks for the call, and your code decides if the call occurs.

const createPayout = betaZodTool({
  name: "create_payout",
  description:
    "Send money from your USD balance to a recipient. This moves real money. " +
    "Always call quote_payout first and show the amounts to the user.",
  inputSchema: z.object({
    rail: z.enum(["bank", "momo"]),
    country: z.string(),
    currency: z.string(),
    amount: z.number().describe("USD to debit from your balance"),
    recipient: z.object({
      account_name: z.string(),
      account_number: z.string(),
      bank_code: z.string().optional(),
    }),
  }),
  run: async (input) => {
    const ok = await askHuman(`Send $${input.amount} to ${input.recipient.account_name}?`);
    if (!ok) return "The user declined this payout. Do not retry it.";
    return figo("/v1/payouts", input);
  },
});

Return the refusal as the tool result. Do not throw an error. The model can then give a sensible answer, and it does not try the same call again.

OpenAI

The tools are the same, but the envelope is different. Describe them as functions, then handle the tool call in your own code.

const tools = [
  {
    type: "function",
    function: {
      name: "quote_collection",
      description:
        "Price a collection before creating one. Read-only — no money moves.",
      parameters: {
        type: "object",
        properties: {
          country: { type: "string", description: "ISO-3166 alpha-2, e.g. NG" },
          currency: { type: "string", description: "Local currency, e.g. NGN" },
          amount: { type: "number" },
          amount_type: { type: "string", enum: ["source", "destination"] },
        },
        required: ["country", "currency", "amount"],
        additionalProperties: false,
      },
    },
  },
];

const res = await openai.chat.completions.create({
  model: OPENAI_MODEL,
  tools,
  messages: [{ role: "user", content: "Price a ₦500,000 collection in Nigeria" }],
});

for (const call of res.choices[0].message.tool_calls ?? []) {
  const args = JSON.parse(call.function.arguments);
  const result = await figo("/v1/collections/quote", args);
  // append { role: "tool", tool_call_id: call.id, content: result } and loop
}

Vercel AI SDK

Do you use the AI SDK now? The tool() function gives the same shape, and it includes the execution. It works with each model provider that you configure.

import { generateText, tool } from "ai";
import { z } from "zod";

const result = await generateText({
  model: yourModel,
  tools: {
    quote_collection: tool({
      description: "Price a collection before creating one. Read-only.",
      inputSchema: z.object({
        country: z.string(),
        currency: z.string(),
        amount: z.number(),
        amount_type: z.enum(["source", "destination"]).optional(),
      }),
      execute: (input) => figo("/v1/collections/quote", input),
    }),
  },
  prompt: "What would collecting ₦500,000 in Nigeria credit me?",
});

MCP

You can give an agent two different things over MCP. Keep them apart.

The first is these docs. We host that server, and it needs no key. The second is your account. That server needs your API key, so it runs on your own machine.

1. Add these docs to your tool

We run an MCP server for this documentation. Connect your editor or your agent to it. The agent can then search every page here directly. You do not copy text, and the agent does not use old data.

The server has one tool, search-documentation. The tool is read-only. It needs no API key, because all the data it reads is already public.

https://docs.spendfigo.com/mcp

Claude Code

claude mcp add --transport http figo-docs https://docs.spendfigo.com/mcp

Cursor, Claude Desktop, Windsurf — add to the client's MCP config (.cursor/mcp.json, claude_desktop_config.json, and so on):

{
  "mcpServers": {
    "figo-docs": {
      "type": "http",
      "url": "https://docs.spendfigo.com/mcp"
    }
  }
}

VS Code — .vscode/mcp.json uses servers rather than mcpServers:

{
  "servers": {
    "figo-docs": {
      "type": "http",
      "url": "https://docs.spendfigo.com/mcp"
    }
  }
}

After you connect it, ask your agent to build against Figo. The agent finds the answers here. It does not guess the shape of an endpoint.

2. Give your agent your account

A search of the docs is not the same as a call to the API. For a call, the agent needs your key.

Therefore you run this server. The key stays in your environment. It does not go to a hosted service, and it does not go into a prompt.

Save this as figo-mcp.mjs:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const BASE_URL = process.env.FIGO_BASE_URL ?? "https://devapi.spendfigo.com";
const API_KEY = process.env.FIGO_API_KEY;
if (!API_KEY) throw new Error("Set FIGO_API_KEY");

const figo = async (path, body) => {
  const res = await fetch(`${BASE_URL}${path}`, {
    method: body ? "POST" : "GET",
    headers: { "x-api-key": API_KEY, "content-type": "application/json" },
    ...(body ? { body: JSON.stringify(body) } : {}),
  });
  return { content: [{ type: "text", text: JSON.stringify(await res.json()) }] };
};

const server = new McpServer({ name: "figo", version: "1.0.0" });

server.registerTool(
  "get_balance",
  { description: "Your current USD balance.", inputSchema: {} },
  () => figo("/v1/balance"),
);

server.registerTool(
  "list_corridors",
  {
    description:
      "Countries you can collect from and pay out to, with methods and limits.",
    inputSchema: {},
  },
  () => figo("/v1/corridors"),
);

server.registerTool(
  "quote_collection",
  {
    description: "Price a collection. Read-only — no money moves.",
    inputSchema: {
      country: z.string().describe("ISO-3166 alpha-2, e.g. NG"),
      currency: z.string().describe("Local currency, e.g. NGN"),
      amount: z.number(),
    },
  },
  (input) => figo("/v1/collections/quote", input),
);

await server.connect(new StdioServerTransport());

Then register it with your key in the environment.

Claude Code

claude mcp add figo -e FIGO_API_KEY=sk_test_your_key_here -- node /absolute/path/to/figo-mcp.mjs

Everything else

{
  "mcpServers": {
    "figo": {
      "command": "node",
      "args": ["/absolute/path/to/figo-mcp.mjs"],
      "env": {
        "FIGO_API_KEY": "sk_test_your_key_here",
        "FIGO_BASE_URL": "https://devapi.spendfigo.com"
      }
    }
  }
}

The default base URL is sandbox. This is deliberate. A change to https://api.spendfigo.com with an sk_live_ key makes the agent move real money. Make that change only after you see that the agent operates correctly.

Look at what the example registers: three read tools, and no tool that moves money.

An MCP server runs for a long time, and the client uses it for every task. Therefore it is the easiest place on this page to give an agent too much power. Add create_collection or create_payout only after you put a confirmation step in the handler. Use the Claude example above as your model.

Ground the agent in these docs

The best method is the MCP server above. But if your tool does not support MCP, you can read every page here as plain text:

/llms.txt An index of every page, for an agent to crawl
/llms-full.txt The entire documentation as one text file
any page + .md e.g. /documentation/getting-started.md
/api-reference/openapi.json The OpenAPI document — every path, field and response shape

All of these are on https://docs.spendfigo.com. An agent that writes code does good work with the docs and the safety rules on this page. Give it both, then ask for the tools that you want.

The site also has an Ask Figo AI button. It answers questions from these pages.

What to reach for

You want Use
A conversational agent that answers about money already moved Read tools only — GET /v1/transactions, GET /v1/balance
An agent that prices things for a human to approve The quote endpoints; no create tools at all
An agent that executes Quote + create, with a confirmation gate inside every create
An agent inside an IDE or desktop client MCP, read tools first

Tip. Run what you build against sk_test_ first. Watch it handle three cases: a refusal, a 400, and a request that it must decline.

The request and response shapes are the same on live. Only the money is different.