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.