# Refunds API

> Refund a payment in full or in part with the Anytime Capital Payments API, safely retried with an Idempotency-Key, within your refund approval limit.

Source: https://anytime-capital.com/docs/refunds · Markdown: https://anytime-capital.com/docs/refunds.md · All docs: https://anytime-capital.com/docs/llms-full.txt

Refund all or part of a payment, never more than is left. Every refund is listed here however it was made (the dashboard, the API, an approved request), each with its own id (`re_…`). The money goes back to the card or account that paid. For the full pattern with retries and approvals, see [Issue refunds safely](https://anytime-capital.com/docs/refund-safely.md).

| Topic | Detail |
| --- | --- |
| Endpoints | [`POST /api/v1/refunds`](#create-refund), [`GET /api/v1/refunds`](#list-refunds), [`GET /api/v1/refunds/{id}`](#get-refund) |
| Key permissions | `refunds:write`, `refunds:read` |
| Samples | curl; Node.js 18+ with the built-in `fetch` (in an ES module); the [Node SDK](https://anytime-capital.com/docs/node-sdk.md) (preview: not on npm yet); Python with `requests`. |
| Every request can also answer | 401 `UNAUTHORIZED`, 401 `API_KEY_EXPIRED`, 403 `NOT_ENROLLED`, 403 `PERMISSION_DENIED`, 403 `IP_NOT_ALLOWED`, 429 `RATE_LIMITED`, 500 `INTERNAL_ERROR`. See [Error codes](https://anytime-capital.com/docs/error-codes.md). |

> **Refund approval limits:** An API key refunds like an admin. If your business has a refund approval limit (**Settings → Team & approvals**) and a refund is over it, nothing is refunded: the answer is `202` with `status: "pending_approval"` and an id starting `re_appr_`, and your owners are asked to approve it. Retrieve that id later: once approved, `refundId` names the refund it became; if declined, `status` is `failed` with `failureReason: "approval_declined"`.

> **Stablecoin payments:** Payments made in stablecoins (ids starting `pay_sc`) went straight to your own wallet, so they're refunded by sending the tokens back from it, never through this endpoint. See [Stablecoin checkout](https://anytime-capital.com/docs/stablecoin-checkout.md#refunds).

## The refund object

```json
{
  "object": "refund",
  "livemode": true,
  "id": "re_8812",
  "paymentId": "pay_7Hq2LmX9cR4tVb",
  "amountCents": 2500,
  "currency": "usd",
  "reason": "Requested by customer",
  "status": "succeeded",
  "failureReason": null,
  "refundId": null,
  "createdAt": "2026-10-20T09:12:44.000Z"
}
```

| Field | Type | Description |
| --- | --- | --- |
| `object` | string | The type of object. Always `refund`. |
| `livemode` | boolean | False for test-mode data (an `ak_test_` key); true for live data. |
| `id` | string | `re_…` for a refund; `re_appr_…` for one that needed an owner's approval. |
| `paymentId` | string | The refunded payment, `pay_…`. |
| `amountCents` | integer | Refunded (or to be refunded). Integer cents. |
| `currency` | string | usd. |
| `reason` | string or null | Why, as recorded: a preset (`requested_by_customer`, `duplicate`, `fraudulent`, `other`) shown as words, your own note, or both. |
| `status` | string | `succeeded`; `pending` (sent, not settled yet); `pending_approval` (waiting for an owner: nothing refunded yet); or `failed`. |
| `failureReason` | string or null | One of `approval_declined`, `failed`, `canceled`. |
| `refundId` | string or null | For an approved `re_appr_…`: the refund it became, `re_…`. |
| `createdAt` | string (date-time) | When it was made or asked for. |

## Endpoints

### Refund a payment

`POST /api/v1/refunds`

Permission: `refunds:write`. Supports `Idempotency-Key`.

Refund all or part of a payment, never more than is left. Over the business's refund approval limit nothing is refunded: the answer is 202 with status pending_approval while an owner decides. Answers `201` with the refund, or `202` with `status: "pending_approval"` and an id starting `re_appr_` when it is over your business's refund approval limit. The Idempotency-Key also keys the refund at the payments network, so a retry after a lost response never refunds twice.

**Headers**

| Header | Type | Required | Description |
| --- | --- | --- | --- |
| `Idempotency-Key` | string | no | Recommended. A unique key such as a UUID: a retry with the same key within 24 hours gets the first response back instead of acting twice. See [Idempotency](https://anytime-capital.com/docs/idempotency.md). |

**Body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `paymentId` | string | yes | The payment to refund, `pay_…`. |
| `amountCents` | integer | no | How much, in cents. Leave out to refund everything still left. At least 1 ($0.01). |
| `reason` | string | no | requested_by_customer, duplicate, fraudulent, other; or your own words. Up to 500 characters. |
| `note` | string | no | Added to a preset reason. Up to 500 characters. |

**Request (curl)**

```bash
curl -X POST https://anytime-capital.com/api/v1/refunds \
  -H "Authorization: Bearer $ANYTIME_API_KEY" \
  -H "Idempotency-Key: refund-order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "paymentId": "pay_7Hq2LmX9cR4tVb",
    "amountCents": 2500,
    "reason": "requested_by_customer"
  }'
```

**Request (Node.js)**

```javascript
const res = await fetch("https://anytime-capital.com/api/v1/refunds", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ANYTIME_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "refund-order-1042",
  },
  body: JSON.stringify({
    paymentId: "pay_7Hq2LmX9cR4tVb",
    amountCents: 2500,
    reason: "requested_by_customer",
  }),
});
const refund = await res.json();
if (!res.ok) throw new Error(`${refund.error.code}: ${refund.error.message}`);
```

**Request (Node SDK)**

```typescript
import { AnytimeCapital } from "@anytimecapital/node";

const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });

const refund = await client.refunds.create(
  {
    paymentId: "pay_7Hq2LmX9cR4tVb",
    amountCents: 2500,
    reason: "requested_by_customer",
  },
  { idempotencyKey: "refund-order-1042" },
);
```

**Request (Python)**

```python
import os

import requests

res = requests.post(
    "https://anytime-capital.com/api/v1/refunds",
    headers={
        "Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}",
        "Idempotency-Key": "refund-order-1042",
    },
    json={
        "paymentId": "pay_7Hq2LmX9cR4tVb",
        "amountCents": 2500,
        "reason": "requested_by_customer",
    },
    timeout=30,
)
refund = res.json()
if not res.ok:
    raise RuntimeError(f"{refund['error']['code']}: {refund['error']['message']}")
```

**Response** (201)

```json
{
  "object": "refund",
  "livemode": true,
  "id": "re_8812",
  "paymentId": "pay_7Hq2LmX9cR4tVb",
  "amountCents": 2500,
  "currency": "usd",
  "reason": "Requested by customer",
  "status": "succeeded",
  "failureReason": null,
  "refundId": null,
  "createdAt": "2026-10-20T09:12:44.000Z"
}
```

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_JSON` | The body isn't valid JSON, or couldn't be read. An empty body reads as `{}`. Send `Content-Type: application/json` and a JSON object. |
| 400 | `INVALID_INPUT` | A field, filter, `limit` or `cursor` is missing or invalid. The message starts with the field's name, as in `amountCents: …`, or reads `Invalid cursor.` Fix the named field. Don't retry the same request unchanged. |
| 400 | `INVALID_IDEMPOTENCY_KEY` | The `Idempotency-Key` header is empty, longer than 255 characters, or has characters other than printable ASCII. Use a UUID. See [Idempotency](https://anytime-capital.com/docs/idempotency.md). |
| 404 | `NOT_FOUND` | No such object on your business (an id from another business reads the same), or no such endpoint under `/api/v1`, which still answers in JSON. Check the id and the path. Ids are case-sensitive. |
| 409 | `NOT_REFUNDABLE` | The payment can't be refunded in its current state, or it was paid in stablecoins, which settle straight to your wallet. Check the payment's `status` and `amountRefundedCents` with `GET /api/v1/payments/{id}`. Refund a stablecoin payment by sending the tokens back from your wallet. |
| 409 | `REFUND_PENDING_APPROVAL` | Another refund on this payment is already waiting for an owner's approval. Wait for the owner's decision: `GET /api/v1/refunds?paymentId=…&status=pending_approval` shows it. |
| 422 | `IDEMPOTENCY_KEY_REUSED` | The `Idempotency-Key` was used in the last 24 hours with a different body or endpoint. Use a new key for a new request; reuse a key only to retry the same request. |
| 500 | `REFUND_NOT_RECORDED` | The refund was sent, but our record of it isn't there yet. Don't refund again. List `GET /api/v1/refunds?paymentId=…` in a minute to find it. |
| 502 | `PROCESSOR_ERROR` | The payments network didn't answer in time. Retry with the same `Idempotency-Key`. A refund is keyed at the network by that key too, so a retry never refunds twice. |
| 503 | `NOT_CONFIGURED` | The part of Anytime Capital Payments this needs isn't available right now. Retry later, with the same `Idempotency-Key` on a write. |

### List refunds

`GET /api/v1/refunds`

Permission: `refunds:read`.

Refunds however they were made, newest first. `status=pending_approval` lists refunds waiting for an owner's approval instead.

**Query**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `paymentId` | string | no | Only refunds of this payment. |
| `status` | string | no | `succeeded`, `pending` or `failed`; or `pending_approval` for the refunds waiting for an owner's approval (a separate list: its cursor works only with the same filter). |
| `limit` | integer | no | How many objects per page, 1–100. Default 25. See [Pagination](https://anytime-capital.com/docs/pagination.md). |
| `cursor` | string | no | The previous page's `nextCursor`, for the page after it. |

**Request (curl)**

```bash
curl "https://anytime-capital.com/api/v1/refunds?paymentId=pay_7Hq2LmX9cR4tVb" \
  -H "Authorization: Bearer $ANYTIME_API_KEY"
```

**Request (Node.js)**

```javascript
const res = await fetch("https://anytime-capital.com/api/v1/refunds?paymentId=pay_7Hq2LmX9cR4tVb", {
  headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` },
});
const page = await res.json();
if (!res.ok) throw new Error(`${page.error.code}: ${page.error.message}`);
```

**Request (Node SDK)**

```typescript
import { AnytimeCapital } from "@anytimecapital/node";

const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });

const page = await client.refunds.list({ paymentId: "pay_7Hq2LmX9cR4tVb" });
```

**Request (Python)**

```python
import os

import requests

res = requests.get(
    "https://anytime-capital.com/api/v1/refunds",
    headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
    params={"paymentId": "pay_7Hq2LmX9cR4tVb"},
    timeout=30,
)
page = res.json()
if not res.ok:
    raise RuntimeError(f"{page['error']['code']}: {page['error']['message']}")
```

**Response** (200)

```json
{
  "object": "list",
  "data": [
    {
      "object": "refund",
      "livemode": true,
      "id": "re_8812",
      "paymentId": "pay_7Hq2LmX9cR4tVb",
      "amountCents": 2500,
      "currency": "usd",
      "reason": "Requested by customer",
      "status": "succeeded",
      "failureReason": null,
      "refundId": null,
      "createdAt": "2026-10-20T09:12:44.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 400 | `INVALID_INPUT` | A field, filter, `limit` or `cursor` is missing or invalid. The message starts with the field's name, as in `amountCents: …`, or reads `Invalid cursor.` Fix the named field. Don't retry the same request unchanged. |

### Get a refund

`GET /api/v1/refunds/{id}`

Permission: `refunds:read`.

One refund (`re_…`) or one that needed approval (`re_appr_…`). For a `re_appr_…` id: `refundId` names the refund it became once an owner approved it; a declined one is `failed` with `failureReason: "approval_declined"`.

**Path**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | yes | `re_…` or `re_appr_…`. |

**Request (curl)**

```bash
curl https://anytime-capital.com/api/v1/refunds/re_8812 \
  -H "Authorization: Bearer $ANYTIME_API_KEY"
```

**Request (Node.js)**

```javascript
const res = await fetch("https://anytime-capital.com/api/v1/refunds/re_8812", {
  headers: { Authorization: `Bearer ${process.env.ANYTIME_API_KEY}` },
});
const refund = await res.json();
if (!res.ok) throw new Error(`${refund.error.code}: ${refund.error.message}`);
```

**Request (Node SDK)**

```typescript
import { AnytimeCapital } from "@anytimecapital/node";

const client = new AnytimeCapital({ apiKey: process.env.ANYTIME_API_KEY! });

const refund = await client.refunds.retrieve("re_8812");
```

**Request (Python)**

```python
import os

import requests

res = requests.get(
    "https://anytime-capital.com/api/v1/refunds/re_8812",
    headers={"Authorization": f"Bearer {os.environ['ANYTIME_API_KEY']}"},
    timeout=30,
)
refund = res.json()
if not res.ok:
    raise RuntimeError(f"{refund['error']['code']}: {refund['error']['message']}")
```

**Response** (200)

```json
{
  "object": "refund",
  "livemode": true,
  "id": "re_8812",
  "paymentId": "pay_7Hq2LmX9cR4tVb",
  "amountCents": 2500,
  "currency": "usd",
  "reason": "Requested by customer",
  "status": "succeeded",
  "failureReason": null,
  "refundId": null,
  "createdAt": "2026-10-20T09:12:44.000Z"
}
```

**Errors**

| Status | Code | When |
| --- | --- | --- |
| 404 | `NOT_FOUND` | No such object on your business (an id from another business reads the same), or no such endpoint under `/api/v1`, which still answers in JSON. Check the id and the path. Ids are case-sensitive. |

**Over the approval limit (202)**

```json
{
  "object": "refund",
  "livemode": true,
  "id": "re_appr_57",
  "paymentId": "pay_7Hq2LmX9cR4tVb",
  "amountCents": 75000,
  "currency": "usd",
  "reason": "Requested by customer",
  "status": "pending_approval",
  "failureReason": null,
  "refundId": null,
  "createdAt": "2026-10-20T09:12:44.000Z"
}
```

## Events

`refund.created` when a refund is made, then `refund.succeeded` or `refund.failed`; the payment's own `payment.refunded` carries `amountRefundedCents`, the total refunded so far. See [Webhooks](https://anytime-capital.com/docs/webhooks.md).

---

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.
