Skip to main content
Cookbooks

Bulk Refund

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

1 min read · updated
View MarkdownEdit on GitHub
TIME
~10 min
STACK
Node.jsno LLM
SERVERS
stripeasaasmercadopago

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

npm install @codespar/sdk

Each settled refund is one transaction — $0.10 + 0.5% cross-border FX if applicable. See Billing.

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.

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:

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:

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

Last updated on