---
title: E-Commerce Checkout
description: Brazilian e-commerce flow — buyer pays via Pix, NF-e gets issued, label generated, customer notified on WhatsApp. Same shape that ships in codespar-core/examples/brazilian-ecommerce.
---

<ActionBar githubPath="content/docs/cookbooks/ecommerce-checkout.mdx" markdownPath="/api/docs-md/cookbooks/ecommerce-checkout" />

<MetaStrip items={[
  { label: "TIME", value: "~15 min" },
  { label: "SDK", value: (<><VersionBadge pkg="@codespar/sdk" /></>) },
  { label: "PROVIDERS", value: (<><ServerChip name="asaas" /><ServerChip name="nfe-io" /><ServerChip name="melhor-envio" /><ServerChip name="z-api" /></>) },
]} />

A buyer pays via Pix, your agent issues an NF-e, generates a Melhor Envio shipping label, and confirms the order on WhatsApp — all driven by four meta-tool calls. This cookbook mirrors the canonical example at [`codespar-core/examples/brazilian-ecommerce/server.ts`](https://github.com/codespar/codespar-core/tree/main/examples/brazilian-ecommerce); the shape is what ships there.

## The Complete Loop

<LoopDiagram
  title="COMPLETE LOOP"
  subtitle="4 meta-tools · one webhook"
  steps={[
    { label: "Charge", tool: "codespar_charge", description: "Pix charge via Asaas (inbound)", iconKey: "card" },
    { label: "Invoice", tool: "codespar_invoice", description: "NF-e via NFe.io (rail: nfe)", iconKey: "doc" },
    { label: "Ship", tool: "codespar_ship", description: "Melhor Envio domestic-label", iconKey: "truck" },
    { label: "Notify", tool: "codespar_notify", description: "WhatsApp confirm via Z-API", iconKey: "chat" },
  ]}
/>

## Prerequisites

Install the SDK:

```bash
npm install @codespar/sdk
```

Set your API key:

```bash title=".env"
CODESPAR_API_KEY=csk_live_...
```

Connect four providers in **/dashboard/connections** before running the agent — Asaas (Pix charges), NFe.io (NF-e issuance), Melhor Envio (shipping labels), and Z-API (WhatsApp). The dashboard walks you through the auth fields each provider needs.

<Callout>
Asaas requires a customer to exist before you can issue a Pix charge against them. The `/whatsapp` flow in the canonical example creates the customer first via the raw `asaas/create_customer` tool — the typed `charge()` wrapper doesn't surface customer creation yet.
</Callout>

## Steps in action

Each step maps to one meta-tool. The Pix charge is **async** — the buyer scans the QR code on their bank app and pays out-of-band. The agent waits for settlement before issuing the NF-e and the shipping label.

<div className="flex flex-col gap-3 my-4">
  <StepCard number={1} title="Charge" meta="meta-tool: codespar_charge · routes to Asaas (Pix BRL)" defaultOpen>
    `session.charge()` is the typed wrapper for `codespar_charge`. It returns immediately with a `tool_call_id`, a Pix QR code, and a copy-paste BR-code. Settlement happens out-of-band when the buyer pays.

    ```typescript
    const charge = await session.charge({
      amount: 14900,
      currency: "BRL",
      method: "pix",
      customer: { id: "cus_..." },
      metadata: { order_id: "order_42" },
    });
    // charge.tool_call_id, charge.pix.qr_code, charge.pix.br_code
    ```
  </StepCard>

  <StepCard number={2} title="Wait for settlement" meta="SSE: paymentStatusStream · low-latency pending → succeeded">
    Don't issue the NF-e until the Pix actually settles. SSE keeps a single connection open with a 15-second heartbeat and pushes each state transition.

    ```typescript
    await session.paymentStatusStream(charge.tool_call_id, {
      onUpdate: (s) => {
        if (s.state === "succeeded") issueNotaFiscal();
      },
    });
    ```

    Polling works too if you prefer — `await session.paymentStatus(charge.tool_call_id)` — but SSE is lower-latency for the typical seconds-to-minutes Pix wait.
  </StepCard>

  <StepCard number={3} title="Invoice" meta="meta-tool: codespar_invoice · rail: nfe · routes to NFe.io">
    NF-e covers physical goods (state-regulated by SEFAZ). NFS-e is the default rail for `codespar_invoice` — pass `rail: "nfe"` for products. No typed wrapper exists yet for invoice; raw `execute()` is fine.

    ```typescript
    const invoice = await session.execute("codespar_invoice", {
      rail: "nfe",
      customer: { name, document, address },
      items: [{ description, amount: 14900, ncm, cfop }],
    });
    ```
  </StepCard>

  <StepCard number={4} title="Ship" meta="meta-tool: codespar_ship · action: label · routes to Melhor Envio">
    `session.ship()` is the typed wrapper for `codespar_ship`. The `domestic-label` rail returns a tracking code and a label PDF URL.

    ```typescript
    const label = await session.ship({
      action: "label",
      origin: { cep: "04538-132" },
      destination: { cep: "01310-100" },
      package: { weight_kg: 0.5, height_cm: 5, width_cm: 15, length_cm: 20 },
    });
    ```
  </StepCard>

  <StepCard number={5} title="Notify" meta="meta-tool: codespar_notify · routes to Z-API (WhatsApp)">
    Confirm the order on WhatsApp with the NF-e access key and the tracking code.

    ```typescript
    await session.execute("codespar_notify", {
      channel: "whatsapp",
      to: customer.phone,
      message: `Pedido confirmado. NF-e ${invoice.access_key}. Rastreio ${label.tracking_code}.`,
    });
    ```
  </StepCard>
</div>

## Full agent code

Drop-in handler for the post-Pix webhook. This is the same shape that ships in `examples/brazilian-ecommerce/server.ts`.

```typescript title="checkout.ts"
import { CodeSpar } from "@codespar/sdk";

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

export async function handlePixPaid(orderId: string, customerId: string) {
  const session = await codespar.create("user_123", {
    servers: ["asaas", "nfe-io", "melhor-envio", "z-api"],
  });

  try {
    // 1. Pix charge (inbound). Returns immediately; settlement is async.
    const charge = await session.charge({
      amount: 14900,
      currency: "BRL",
      method: "pix",
      customer: { id: customerId },
      metadata: { order_id: orderId },
    });

    // 2. Wait for the buyer to pay before issuing the NF-e.
    await session.paymentStatusStream(charge.tool_call_id, {
      onUpdate: (s) => {
        if (s.state === "failed") throw new Error(`Pix failed: ${s.events.at(-1)}`);
      },
    });

    // 3. NF-e (products, state-regulated).
    const invoice = await session.execute("codespar_invoice", {
      rail: "nfe",
      customer: { /* … */ },
      items: [{ description: "Starter Kit", amount: 14900, ncm: "8471.30.12", cfop: "5102" }],
    });

    // 4. Melhor Envio domestic label.
    const label = await session.ship({
      action: "label",
      origin: { cep: "04538-132" },
      destination: { cep: "01310-100" },
      package: { weight_kg: 0.5, height_cm: 5, width_cm: 15, length_cm: 20 },
    });

    // 5. WhatsApp confirmation.
    await session.execute("codespar_notify", {
      channel: "whatsapp",
      to: "+5511999887766",
      message: `Pedido confirmado. NF-e ${invoice.data.access_key}. Rastreio ${label.data.tracking_code}.`,
    });
  } finally {
    await session.close();
  }
}
```

<Callout type="warn">
**Don't issue the NF-e before settlement.** A NF-e can only be cancelled within a short SEFAZ window (typically 24h, varies by state). If you issue based on the charge response and the buyer never pays, you've created a phantom invoice that needs cancellation paperwork. Always wait on `paymentStatusStream` (or `paymentStatus`) until `succeeded`.
</Callout>

## Variations

### Boleto fallback
Asaas supports both Pix and boleto on `codespar_charge`. Pass `method: "boleto"` and the response carries a barcode + PDF URL instead of a Pix QR code. Settlement is T+1 to T+3 business days; the same `paymentStatusStream` correlation works.

### NFS-e instead of NF-e
If you sell services rather than products, drop the `rail: "nfe"` argument — `codespar_invoice` defaults to NFS-e (municipal). NFS-e doesn't need an NCM/CFOP classification.

### Multi-tenant
Pass the real `userId` as the first argument to `codespar.create(userId, config)` to isolate credentials and sessions per end-customer. See [Multi-Tenant Agent](/docs/cookbooks/multi-tenant).

### Card USD instead of Pix BRL
Set `currency: "USD"` and `method: "card"` on `session.charge(...)` — the router picks Stripe instead of Asaas / MP / iugu / Stone.

## Next steps

<NextStepsGrid items={[
  { label: "EXAMPLE", title: "brazilian-ecommerce on GitHub", description: "The runnable end-to-end example this cookbook is based on.", href: "https://github.com/codespar/codespar-core/tree/main/examples/brazilian-ecommerce" },
  { label: "COOKBOOK", title: "Pix Payment Agent", description: "A focused, minimal agent just for creating Pix charges and notifying customers via WhatsApp.", href: "/docs/cookbooks/pix-payment-agent" },
  { label: "COOKBOOK", title: "Multi-Provider Agent", description: "Route payments to the cheapest provider per transaction.", href: "/docs/cookbooks/multi-provider" },
  { label: "CONCEPT", title: "Tools & Meta-Tools", description: "Nine meta-tools, 53 routing rails, the routing model.", href: "/docs/concepts/tools" },
  { label: "REFERENCE", title: "Sessions API", description: "Full API reference for sessions and the typed wrappers.", href: "/docs/api/sessions" },
]} />
