PersonalBusinessEnterprise
    In a rush? Schedule an Appointment
    Fetching local time...
    Anytime CapitalforBusiness
    Pricing

    Get started

    • Overview
    • Quickstart
    • Test mode
    • Guides & recipes
    • Switch from Stripe
    • Import from Shopify
    • Import from WooCommerce
    • Import from Square
    • Import a CSV file
    • Stripe API mapping
    • Pricing
    • Support
    • FAQ

    Payments

    • Checkout & website
    • Guide: Next.js checkout
    • In person (QR)
    • Payment methods
    • Stablecoin checkout
    • Guide: take stablecoins
    • Fraud rules & 3D Secure
    • Dispute evidence
    • International customers
    • Guide: safe refunds
    • Bank transfers on invoices

    Billing

    • Subscription discounts
    • Guide: sell subscriptions
    • Usage billing
    • Guide: report usage

    Money movement

    • Payout options
    • Guide: reconcile payouts
    • Accounting sync
    • Revenue analytics

    Developers

    • Authentication
    • Errors & retries
    • Pagination
    • Idempotency
    • Rate limits
    • Webhooks
    • Verify webhooks
    • Node SDK (preview)
    • CLI (preview)
    • Postman collection

    AI

    • AI agents hub
    • Safe AI agents
    • Custom agents
    • Prompt library

    Integrations

    • Website builders
    • Shopify
    • Shopify orders by invoice
    • WooCommerce
    • Zapier
    • Slack, Discord and Teams

    Reference

    • Payments
    • Refunds
    • Disputes
    • Payment links
    • Invoices
    • Customers
    • Products
    • Subscriptions & plans
    • Discount codes
    • Meters & usage events
    • Payouts & balance
    • Webhook endpoints
    • Events
    • Test helpers
    • OpenAPI spec
    • Error codes
    • Changelog
    • Glossary

    For AI tools

    • llms-full.txt
    • MCP server
    • openapi.json
    Docs menu · Guide: safe refunds

    Get started

    • Overview
    • Quickstart
    • Test mode
    • Guides & recipes
    • Switch from Stripe
    • Import from Shopify
    • Import from WooCommerce
    • Import from Square
    • Import a CSV file
    • Stripe API mapping
    • Pricing
    • Support
    • FAQ

    Payments

    • Checkout & website
    • Guide: Next.js checkout
    • In person (QR)
    • Payment methods
    • Stablecoin checkout
    • Guide: take stablecoins
    • Fraud rules & 3D Secure
    • Dispute evidence
    • International customers
    • Guide: safe refunds
    • Bank transfers on invoices

    Billing

    • Subscription discounts
    • Guide: sell subscriptions
    • Usage billing
    • Guide: report usage

    Money movement

    • Payout options
    • Guide: reconcile payouts
    • Accounting sync
    • Revenue analytics

    Developers

    • Authentication
    • Errors & retries
    • Pagination
    • Idempotency
    • Rate limits
    • Webhooks
    • Verify webhooks
    • Node SDK (preview)
    • CLI (preview)
    • Postman collection

    AI

    • AI agents hub
    • Safe AI agents
    • Custom agents
    • Prompt library

    Integrations

    • Website builders
    • Shopify
    • Shopify orders by invoice
    • WooCommerce
    • Zapier
    • Slack, Discord and Teams

    Reference

    • Payments
    • Refunds
    • Disputes
    • Payment links
    • Invoices
    • Customers
    • Products
    • Subscriptions & plans
    • Discount codes
    • Meters & usage events
    • Payouts & balance
    • Webhook endpoints
    • Events
    • Test helpers
    • OpenAPI spec
    • Error codes
    • Changelog
    • Glossary

    For AI tools

    • llms-full.txt
    • MCP server
    • openapi.json
    DocsPaymentsGuide: safe refunds

    Issue refunds safely with idempotency

    Refund payments from your code without ever refunding twice: a stored Idempotency-Key, retries that are safe, refunds waiting for approval, and events.

    View as MarkdownOpen in Claude

    A refund moves money back to a customer, so it must happen exactly once even when networks fail. The pattern: decide the refund and store its Idempotency-Key first, send it with that key as many times as you need, and read the outcome. The key also keys the refund at the payments network, so even a retry after a lost response refunds once. Endpoint details are on Refunds.

    What can come back

    AnswerMeaningDo
    201, status: succeededRefunded.Done.
    201, status: pendingSent, not settled yet.Wait for refund.succeeded or refund.failed.
    202, status: pending_approvalOver your approval limit: an owner decides. Nothing refunded yet.Keep the re_appr_… id; check it later or wait for refund.created.
    409 REFUND_PENDING_APPROVALAnother refund on this payment already waits for approval.Don't send another; wait.
    409 NOT_REFUNDABLE, 400 INVALID_INPUTCan't be refunded, or more than is left.Show a person; don't retry unchanged.
    429, 502, 503, 500 INTERNAL_ERROR, no answerNot done, or not known.Retry with the same key.
    500 REFUND_NOT_RECORDEDSent, but our record isn't there yet.Never resend: list GET /api/v1/refunds?paymentId=… in a minute.

    Node.js

    refunds.mjs
    // refunds.mjs — Node.js 18+. Run: node refunds.mjs pay_… 2500
    import { randomUUID } from "node:crypto";
    
    const API = "https://anytime-capital.com/api/v1";
    const headers = { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` };
    
    // 1. Decide the refund once, and give it a key you store with it (a table row in real life).
    export function planRefund(paymentId, amountCents) {
      return { paymentId, amountCents, idempotencyKey: `refund-${randomUUID()}`, state: "planned", refundId: null };
    }
    
    // 2. Send it. Safe to call again and again with the same plan: the key makes it happen once.
    export async function sendRefund(plan) {
      for (let attempt = 1; attempt <= 4; attempt++) {
        let res;
        try {
          res = await fetch(`${API}/refunds`, {
            method: "POST",
            headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": plan.idempotencyKey },
            body: JSON.stringify({ paymentId: plan.paymentId, amountCents: plan.amountCents, reason: "requested_by_customer" }),
          });
        } catch {
          await sleep(2 ** attempt); // the network failed: same key, try again
          continue;
        }
        const body = await res.json().catch(() => ({}));
    
        if (res.status === 201) return { ...plan, state: body.status, refundId: body.id }; // succeeded or pending
        if (res.status === 202) return { ...plan, state: "pending_approval", refundId: body.id }; // re_appr_…: an owner decides
    
        const code = body.error?.code;
        if (code === "REFUND_NOT_RECORDED") return { ...plan, state: "sent_unrecorded" }; // it went out: look it up, never resend
        if (res.status === 429 || res.status >= 500) {
          await sleep(Number(res.headers.get("Retry-After") ?? 2 ** attempt));
          continue; // RATE_LIMITED, PROCESSOR_ERROR, NOT_CONFIGURED, INTERNAL_ERROR: same key again
        }
        // NOT_FOUND, NOT_REFUNDABLE, REFUND_PENDING_APPROVAL, INVALID_INPUT…: a person needs to look.
        return { ...plan, state: "failed", error: `${code}: ${body.error?.message}` };
      }
      return { ...plan, state: "retry_later" }; // run sendRefund(plan) again later with the same plan
    }
    
    // 3. Later (or on a webhook): where does a refund that waited for approval stand?
    export async function checkRefund(refundId) {
      const res = await fetch(`${API}/refunds/${refundId}`, { headers });
      const refund = await res.json();
      if (!res.ok) throw new Error(`${refund.error.code}: ${refund.error.message}`);
      if (refund.status === "failed" && refund.failureReason === "approval_declined") return "declined";
      if (refund.refundId) return `approved as ${refund.refundId}`;
      return refund.status;
    }
    
    const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
    
    if (process.argv[2]) {
      const plan = planRefund(process.argv[2], process.argv[3] ? Number(process.argv[3]) : undefined);
      console.log(await sendRefund(plan));
    }

    Python

    refunds.py
    # refunds.py — pip install requests. Run: python refunds.py pay_… 2500
    import os
    import sys
    import time
    import uuid
    
    import requests
    
    API = "https://anytime-capital.com/api/v1"
    HEADERS = {"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"}
    
    
    def plan_refund(payment_id, amount_cents=None):
        """Decide the refund once and store its key with it."""
        return {"payment_id": payment_id, "amount_cents": amount_cents, "key": f"refund-{uuid.uuid4()}", "state": "planned"}
    
    
    def send_refund(plan):
        """Safe to call again with the same plan: the key makes the refund happen once."""
        body = {"paymentId": plan["payment_id"], "reason": "requested_by_customer"}
        if plan["amount_cents"] is not None:
            body["amountCents"] = plan["amount_cents"]
        for attempt in range(1, 5):
            try:
                res = requests.post(f"{API}/refunds", headers={**HEADERS, "Idempotency-Key": plan["key"]}, json=body, timeout=30)
            except requests.RequestException:
                time.sleep(2**attempt)  # the network failed: same key, try again
                continue
            try:
                data = res.json()
            except ValueError:
                data = {}
            if res.status_code == 201:
                return {**plan, "state": data["status"], "refund_id": data["id"]}
            if res.status_code == 202:
                return {**plan, "state": "pending_approval", "refund_id": data["id"]}
            code = data.get("error", {}).get("code")
            if code == "REFUND_NOT_RECORDED":
                return {**plan, "state": "sent_unrecorded"}  # it went out: look it up, never resend
            if res.status_code == 429 or res.status_code >= 500:
                time.sleep(int(res.headers.get("Retry-After", 2**attempt)))
                continue
            return {**plan, "state": "failed", "error": f"{code}: {data.get('error', {}).get('message')}"}
        return {**plan, "state": "retry_later"}
    
    
    if __name__ == "__main__":
        amount = int(sys.argv[2]) if len(sys.argv) > 2 else None
        print(send_refund(plan_refund(sys.argv[1], amount)))

    Partial refunds

    • Send amountCents for part of a payment; leave it out to refund everything still left.
    • Several partial refunds are fine, each with its own key, up to what was paid. Over that answers 400 INVALID_INPUT naming amountCents.
    • On payment.refunded, data.amountRefundedCents is the total refunded so far, not this refund alone.

    Keep a person in the loop

    • Set a refund approval limit in Settings → Team & approvals: refunds over it by admins, API keys and AI agents wait for an owner. A limit of $0 makes every one of them wait.
    • Give the code that refunds its own restricted key with refunds write and nothing it doesn't need.
    • Every refund is in your activity log under the key's name.

    Stablecoin payments (pay_sc…) are refunded from your own wallet, not with this endpoint. See Stablecoin checkout.

    PreviousInternational customersNext Bank transfers on invoices

    Anytime Capital is a fintech company, not a bank. Payment processing, banking and payout services are provided by Anytime Capital's licensed partners. Rates shown are for US card payments; international cards, currency conversion and disputes may carry additional fees shown in your dashboard before you accept a payment.

    On this page

    • What can come back
    • Node.js
    • Python
    • Partial refunds
    • Keep a person in the loop
    Anytime Capital
    (470) 748-2530
    App Store

    Business

    • Business Checking
    • Business Debit Cards
    • Payments
    • Ecommerce Payments
    • Invoicing
    • Developer Docs

    Solutions

    • CPAs & Accountants
    • Financial Advisors
    • Boat & Auto Dealerships
    • Dental & Medical Practices
    • Creators & Coaches
    • All solutions

    Crypto

    • Crypto Wallet
    • Tether Wallet
    • Buy Digital Assets
    • Explore Markets
    • Security & Licenses
    • Compare Platforms
    • Bitcoin ATM Alternative

    Company

    • Our Story
    • Schedule Appointment
    • Careers
    • Media & Press
    • Investor Relations
    • FAQ

    Branches

    • Atlanta, Georgia
    • Miami, Florida
    • Houston, Texas
    • All branches

    Legal

    • Terms & Conditions
    • Privacy Policy
    • Your Privacy Choices
    • Fraud Notice

    © 2026 Anytime Capital LLC. Anytime Capital is a fintech company, not a bank.

    The Anytime Capital Mastercard® Prepaid Card is issued by Pathward®, N.A., Member FDIC. Mastercard® is a registered trademark of Mastercard International Incorporated. All other trademarks and service marks belong to their respective owners.

    Mastercard