Skip to main content

TutorialUpdated 2026.10.079 min read

How to Build a Payment Agent with CodeSpar

A step-by-step guide: install the SDK, create a session, charge via Pix, issue an NF-e, notify the customer on WhatsApp, and chain from a single agent the four Complete Loop steps that are available as meta tools, within the credentials, permissions and approvals configured. Real code. Real APIs. Production-ready patterns.
Fabiano Cruz

Why agents need payment capabilities

The average e-commerce operation in Brazil involves six systems per sale: a payment gateway to charge via Pix, a fiscal service to issue the NF-e, a logistics platform to generate a shipping label, WhatsApp to notify the customer, an ERP to record the transaction, and a banking API to reconcile the funds. A human operator touches all six. For every order.

AI agents can orchestrate these systems faster, more reliably, and at a fraction of the cost. But they need the right tools. An LLM that "knows about Pix" cannot actually call a Pix provider's API, handle the webhook for payment confirmation, or parse SEFAZ's response codes. Knowledge without tools is trivia. Tools without orchestration is plumbing. You need both.

This tutorial walks you through building an agent that handles the complete commerce flow using the CodeSpar SDK. By the end, you'll have an agent that charges via Pix, issues an NF-e, and notifies the customer on WhatsApp. Then we'll combine all three into a single Complete Loop call.

Prerequisites

You need: Node.js 20+; a CodeSpar account, which comes with a test project and a test key (csk_test_…) at Dashboard → API Keys; and the providers these examples use, connected in the project: a charge provider for Pix, NFe.io for the NF-e (with the A1 certificate uploaded to your NFe.io company, the state tax registration, the tax regime and each item's ICMS, IPI, PIS and COFINS classification), Z-API for WhatsApp (an instance paired with your WhatsApp number, with its instance ID, instance token and Client-Token connected in the project), and a carrier for the shipping label. Under a test key, each call goes to its provider's test environment; NFe.io and Z-API have none, so the NF-e and the WhatsApp message are refused under a test key and run only in a live project, where they take real effect. The examples compile against @codespar/sdk 0.16.11; they were not run end to end for this article.

Start by installing the SDK:

npm install @codespar/sdk@0.16.11

Get your API key from the CodeSpar dashboard. Test mode is free, which is more than enough for this tutorial.

Environment setup

Create a .env file in your project root:

CODESPAR_API_KEY=csk_test_...

A test key keeps every tool call in test: each one goes to the provider's test environment, and a provider without one is refused instead of reaching production. No real charges. No real invoices. Switch to a live key when you're ready.

Step 1: Create a session with payment servers

Every interaction with CodeSpar starts with a session. A session is an immutable context container that tells the SDK which MCP servers to connect to, which credentials to use, and which user is making the request.

import { CodeSpar, tools } from "@codespar/sdk";

const codespar = new CodeSpar({
  apiKey: process.env.CODESPAR_API_KEY,
});

// Create a session with the Brazilian commerce preset
const session = await codespar.create("user_abc123", {
  preset: "brazilian",
});

console.log(session.id);
console.log((await tools(session)).map((t) => t.name));
// → the meta tools: "codespar_charge", "codespar_invoice", "codespar_notify", ...

The preset: "brazilian" flag is important. It tells the SDK which servers to load for Brazilian commerce: zoop, nuvem-fiscal, melhor-envio, z-api and omie. The meta tools route each call to the provider connected in your project: a Pix charge to your Pix provider (Asaas, Mercado Pago, Celcoin and others in the catalog), the NF-e to NFe.io, WhatsApp to Z-API and labels to Melhor Envio. Omie has no meta tool.

You can also specify individual servers with the servers array.

Why sessions?

Sessions solve three problems at once. First, credential isolation: each session binds to a specific user's OAuth tokens and API keys. Second, context scoping: the LLM only sees the tools available in the session, not the entire catalog. Third, usage attribution: every tool call in a session is logged against the organization that created it. Logging is not billing: tool calls are not billed, and billing follows the pricing table.

Step 2: Execute a Pix charge

With the session created, you can call codespar_charge to charge a customer via Pix (its counterpart, codespar_pay, sends money out). This is a meta tool: it doesn't call a provider's API directly. CodeSpar routes it to the Pix provider connected in your project (in Brazil, the catalog lists Asaas, Mercado Pago, EBANX, Celcoin, Iugu and Stone for Pix charges) and calls it with that connection's credentials.

const charge = await session.execute("codespar_charge", {
  method: "pix",
  amount: 150,          // R$150.00, in reais
  currency: "BRL",
  description: "Premium Plan - April 2026",
  buyer: {
    name: "Maria Silva",
    document: "12345678900",
    email: "maria@example.com",
  },
});

console.log(charge.success, charge.data);
// charge.data carries the Pix copy-and-paste code (pix_copy_paste)
// and the QR code (pix_qr_code) to send to the customer.

The response includes a Pix copy-and-paste code and a QR code. You can send either to the customer.

Handling payment confirmation

Pix payments confirm in real time. When the customer scans the QR code and authorizes the payment, the BACEN (Central Bank) settles the funds instantly. CodeSpar listens for the provider's webhook and records the payment's status:

const charge = await session.execute("codespar_charge", {
  method: "pix",
  amount: 150,
  currency: "BRL",
  description: "Premium Plan - April 2026",
  buyer: { name: "Maria Silva", document: "12345678900" },
});

// Option A: read the charge's status
const { payment_status } = await session.paymentStatus(charge.tool_call_id!);
// → "pending" until Maria pays, then "succeeded"

// Option B: webhook (production)
// Subscribe a trigger to the charge's events in the dashboard
// (/docs/concepts/triggers); CodeSpar POSTs them to your endpoint.

In test mode, how a payment gets confirmed depends on the provider's test environment. In production, you'll want to use webhooks for reliability.

Pix settles in under 10 seconds. Compare that to credit cards (D+30 in Brazil) or ACH in the US (2-3 business days). Instant settlement makes Pix structurally superior for agent-driven commerce.

Step 3: Issue the NF-e automatically

In Brazil, every sale requires a Nota Fiscal Eletronica (NF-e). This isn't optional. SEFAZ (the state tax authority) validates every invoice in real time. An incorrect CFOP, a wrong ICMS rate, or a malformed XML and the invoice is rejected. Businesses face fines for issuing invoices late or incorrectly.

The codespar_invoice meta tool sends the invoice to NFe.io, which generates and signs the XML and submits it to SEFAZ. Each item's tax classification and the fiscal setup come from you (below).

const invoice = await session.execute("codespar_invoice", {
  action: "issue",
  type: "nfe",
  recipient: {
    name: "Maria Silva",
    document: "12345678900",
    email: "maria@example.com",
  },
  items: [
    {
      description: "Premium Plan - SaaS",
      quantity: 1,
      unit_price: 150,
    },
  ],
});

console.log(invoice.success, invoice.data);
What the NF-e needs

Tax classification: ICMS rates vary across 26 Brazilian states. A product shipped from SP to RJ has a different rate than SP to BA. Each item needs its ICMS, IPI, PIS and COFINS classification (CST/CSOSN) on the NFe.io side; without it, NFe.io refuses the NF-e.

XML signing: NF-e requires a valid A1 digital certificate. It is uploaded to your NFe.io company, and NFe.io signs the XML with it. Renewing the certificate before it expires is up to you.

Step 4: Notify the customer via WhatsApp

In Latin America, WhatsApp isn't a nice-to-have notification channel. It's the primary channel. Over 500 million people in the region use it daily. Customers expect order confirmations, payment receipts, and tracking updates on WhatsApp, not email.

The codespar_notify meta tool routes to the Z-API MCP server:

const notification = await session.execute("codespar_notify", {
  channel: "whatsapp",
  to: "5511999887766", // country code + number, digits only
  message: "Olá, Maria! Recebemos seu pagamento de R$ 150,00 (Premium Plan - April 2026).",
});

console.log(notification.success, notification.data);

On this rail, codespar_notify sends the message as text through Z-API, to the number in to (country code and number, digits only). When Z-API accepts the send-text request, codespar_notify returns the message id Z-API sent back, the channel, the recipient and the status QUEUED. QUEUED means only that Z-API accepted the request: it does not say the message was delivered or read.

Why not email?

Open rates tell the story. WhatsApp messages in Brazil have a 90%+ open rate. Email marketing sits around 15-20%. For transactional messages (payment confirmation, shipping updates), WhatsApp delivery is nearly instant.

Step 5: The Complete Loop

Steps 2-4 each call a single tool. That's useful for testing, but in production you want the agent to chain the steps, within the credentials, permissions and approvals configured in your project. This is what the Complete Loop does: charge, invoice, ship, notify — in one call. ERP registration and bank reconciliation have no meta tool today, so this loop stops at the notification.

First, create a session with the full preset:

const session = await codespar.create("user_abc123", {
  preset: "brazilian",
  // The preset's servers: zoop, nuvem-fiscal, melhor-envio,
  // z-api, omie. codespar_invoice issues the NF-e through NFe.io.
});

Then send a natural language instruction:

const run = await session.send(`
  Charge Maria Silva (CPF 123.456.789-00) R$150 via Pix
  for Premium Plan. Issue the NF-e for the sale.
  Generate a shipping label for a 500g package
  from CEP 01310-100 to CEP 22041-080.
  Notify Maria on WhatsApp (+5511999887766) with the
  payment details and the tracking code.
`);

for (const call of run.tool_calls) {
  console.log(call.tool_name, call.status);
}
// codespar_charge, codespar_invoice, codespar_ship, codespar_notify:
// one tool call per step, in the order the agent ran them.

Four meta tools. One natural language instruction. This is what the Complete Loop looks like in code.

session.send() hands the instruction to the agent, which calls the meta tools in dependency order: the charge before the invoice, the invoice before the label, and so on. Each call comes back in run.tool_calls. When you want a fixed order instead, loop(session, { steps }) runs the steps you list, one after another, and onStepComplete reports each one.

What's happening under the hood

When you call session.send() or loop(session, ...), three layers process the request:

1. Meta tool routing

A Brazilian commerce preset loads several MCP servers, each with its own raw tools. Exposing all of them to an LLM wastes context window tokens and increases the chance of tool selection errors. Instead, the SDK compresses them into meta tools, among them codespar_charge, codespar_invoice, codespar_ship and codespar_notify.

Each meta tool acts as a router. When the agent calls codespar_charge with method: "pix", the meta tool resolves this to the charge tool of the provider your organization connected (Asaas create_payment, for example). If you passed method: "boleto", it would route to that provider's boleto instead. The agent doesn't need to know which underlying tool to call — the meta tool handles selection.

2. Managed authentication

Each provider takes different credentials. Zoop and NFe.io take an API key. Z-API takes an instance ID, an instance token and a Client-Token. Melhor Envio uses OAuth. CodeSpar keeps them in its vault, per user connection, and resolves them on each call. You never pass raw credentials in tool calls.

3. Framework adapters

The SDK works with any LLM framework. The tools(session) function returns tool definitions compatible with Claude, OpenAI, and the Vercel AI SDK:

// With Anthropic's Claude SDK
import Anthropic from "@anthropic-ai/sdk";
import { tools } from "@codespar/sdk";

const anthropic = new Anthropic();
const session = await codespar.create("user_abc123", {
  preset: "brazilian",
});

const response = await anthropic.messages.create({
  model: "claude-sonnet-4-20250514",
  max_tokens: 4096,
  tools: (await tools(session)).map(({ name, description, input_schema }) => ({
    name,
    description,
    input_schema: { ...input_schema, type: "object" as const },
  })),
  messages: [
    { role: "user", content: "Charge R$150 via Pix to Maria" },
  ],
});

// Handle tool use in the response
for (const block of response.content) {
  if (block.type === "tool_use") {
    const result = await session.execute(block.name, block.input as Record<string, unknown>);
    // Feed result back to Claude for next step
  }
}
15meta tools in @codespar/types 0.11.4
0raw credentials in code
3framework adapters

Going to production

The sandbox gets you through development. Production requires three additional considerations.

Billing and quotas

CodeSpar prices by settled money movement, not by tool call. The payment that settles is the billable event; the invoice, label, notification, and ERP steps around it are included in that transaction price. Test mode is free, and Enterprise contracts negotiate volume pricing separately. Current rates are on the pricing page.

Error handling

Production commerce fails in predictable ways. Pix charges expire. SEFAZ goes down. WhatsApp rate-limits you. The SDK surfaces them in two places: a tool result that did not succeed, and a typed error when the API refuses the request or does not answer in time:

import { CodesparApiError, TimeoutError } from "@codespar/sdk";

try {
  const charge = await session.execute("codespar_charge", {
    method: "pix",
    amount: 150,
    currency: "BRL",
    description: "Premium Plan - April 2026",
    buyer: { name: "Maria Silva" },
  });
  if (!charge.success) {
    // The provider refused the charge: charge.error says why
    console.error(charge.error);
  }
} catch (error) {
  if (error instanceof TimeoutError) {
    // No answer in time: read the status before charging again
  }
  if (error instanceof CodesparApiError) {
    // The API refused the request: status, code and message
    console.error(error.status, error.code, error.message);
  }
}

Monitoring

The dashboard provides real-time visibility into every session: tool calls, latencies, error rates, and billing. You can also feed Datadog, Grafana, or any OpenTelemetry-compatible system from session.sendStream(), which yields each tool call and its result as it happens.

// Feed your observability stack from the session's stream
for await (const event of session.sendStream("Charge R$150 via Pix to Maria")) {
  if (event.type === "tool_result") {
    const { tool_name, server_id, status, duration_ms } = event.toolCall;
    console.log(JSON.stringify({ metric: "codespar.tool.duration", duration_ms, tool_name, server_id, status }));
  }
  if (event.type === "error") {
    console.error(event.error, event.message);
  }
}

What you've built

In this tutorial, you've built an agent that:

1. Charges via Pix through codespar_charge, with a QR code and a copy-and-paste code.

2. Issues an NF-e through codespar_invoice, once the fiscal setup in the prerequisites is in place.

3. Notifies the customer on WhatsApp through Z-API, as a text message.

4. Runs the Complete Loop: the charge, invoice, shipping and notification steps from a single natural language instruction.

These are the same meta tools a live key calls in production. The code does not change between your test session and a production session; the key does.

The code you wrote in test is the code you run in production. That's what infrastructure should feel like.

Next steps

Try the Sandbox: Test tool calls interactively in the CodeSpar Sandbox.

Read the Complete Loop deep dive: Understand every step, every edge case, and every compliance requirement in our Complete Loop deep dive.

Browse the MCP catalog: See all 50+ servers and 400+ tools on GitHub.

Fabiano Cruz is co-founder of CodeSpar. The SDK is available at npm. All MCP servers referenced in this tutorial are open source on GitHub.

Updated on 2026.10.07: the examples now compile against @codespar/sdk 0.16.11: the charge uses codespar_charge (codespar_pay sends money out), a test key replaces the CODESPAR_ENV variable no SDK version reads, the loop covers the four steps that have a meta tool, unmeasured timings are gone, and the sandbox and catalog links point to their current pages. The Pix charge goes to the provider connected in your project, not to Zoop, and the raw-tool count is gone. Tool calls are logged, not billed, and QUEUED means only that Z-API accepted the request.

Published 2026.04.15 · Fabiano Cruz#tutorial

More to read

Next in Tutorial.

All posts

Build agents that can move money.

Docs, cookbooks and a sandbox with test keys.

Test keys are free.

terminal · test modedemo
$ npm install @codespar/sdk$ export CODESPAR_API_KEY=csk_test_…
How to Build a Payment Agent with CodeSpar