---
title: Bulk Refund
description: Refund a batch of payments in parallel using session.loop(). Handle partial failures without aborting the whole run.
---

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

<MetaStrip items={[
  { label: "TIME", value: "~10 min" },
  { label: "STACK", value: (<><ServerChip name="Node.js" accent /><span style={{ color: "var(--color-fd-muted-foreground)", fontSize: 12 }}>no LLM</span></>) },
  { label: "SERVERS", value: (<><ServerChip name="stripe" /><ServerChip name="asaas" /><ServerChip name="mercadopago" /></>) },
]} />

Refund a batch of payments in one server-side job. Use `session.loop()` with `abortOnError: false` so one provider error does not kill the whole run. Good fit for chargeback reconciliation, marketplace seller disputes, and end-of-day cleanup workers.

## Prerequisites

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

<Callout>
Each settled refund is **one transaction** — $0.10 + 0.5% cross-border FX if applicable. See [Billing](/docs/concepts/billing).
</Callout>

## The loop

`session.loop()` runs N steps in order, but the steps are independent — each is a `codespar_pay` refund against a specific payment. `abortOnError: false` lets the successful ones settle even if some fail.

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

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

interface RefundJob {
  payment_id: string;
  amount: number; // centavos
  reason: string;
}

async function bulkRefund(jobs: RefundJob[]) {
  const session = await codespar.create("refund-worker", {
    servers: ["stripe", "asaas", "mercadopago"],
    metadata: { source: "cron:bulk-refund" },
  });

  try {
    const result = await session.loop({
      steps: jobs.map((job) => ({
        tool: "codespar_pay",
        params: {
          action: "refund",
          payment_id: job.payment_id,
          amount: job.amount,
          reason: job.reason,
        },
      })),
      abortOnError: false,
      onStepError: (step, error, index) => {
        console.error(`Refund ${index} failed`, step.params, error);
      },
    });

    return {
      total: jobs.length,
      succeeded: result.results.filter((r) => r.success).length,
      failed: result.results.filter((r) => !r.success),
    };
  } finally {
    await session.close();
  }
}
```

## Handling the failures

`result.results` is a parallel array to the input `jobs`. Each entry has `success`, `data`, `error`, and the provider that handled it — so you can retry, alert, or write to a dead-letter queue per-job:

```typescript
const summary = await bulkRefund(todaysChargebacks);

for (const failure of summary.failed) {
  await deadLetterQueue.add({
    tool_call_id: failure.tool_call_id,
    error: failure.error,
    payload: failure.data,
  });
}

metrics.increment("refund.bulk.succeeded", summary.succeeded);
metrics.increment("refund.bulk.failed", summary.failed.length);
```

## Idempotency

Provider APIs return the same refund if you call them twice with the same `payment_id` (at least for Stripe, Asaas, Pagar.me, and Mercado Pago). CodeSpar forwards the call verbatim — so re-running the same bulk job is safe. If you need stricter guarantees, use your own idempotency table keyed on `payment_id + date`.

## Variations

### Pre-flight check with `codespar_discover`
Call `codespar_discover` once before the loop to confirm which providers are connected for refunds and avoid N wasted attempts when a provider is down:

```typescript
const capabilities = await session.execute("codespar_discover", {
  domain: "payments",
  capability: "refund",
});
// Only proceed if capabilities.servers.length > 0
```

### Parallel vs sequential
`session.loop()` runs steps sequentially by design — it is predictable and easy to debug. For large batches (1000+), chunk into groups of 50 and run chunks in parallel with `Promise.all`, reopening a session per chunk if you hit the 30-min inactivity timeout.

## Next steps

<NextStepsGrid items={[
  { label: "COOKBOOK", title: "Webhook Listener", description: "Trigger bulk-refund from a chargeback.created webhook event.", href: "/docs/cookbooks/webhook-listener" },
  { label: "COOKBOOK", title: "Multi-Provider Agent", description: "Let the router pick which provider gets each refund.", href: "/docs/cookbooks/multi-provider" },
  { label: "CONCEPT", title: "Billing", description: "Refunds count as settled transactions — how pricing applies.", href: "/docs/concepts/billing" },
  { label: "CONCEPT", title: "Sessions", description: "session.loop() reference — abortOnError, onStepComplete, retries.", href: "/docs/concepts/sessions" },
]} />
