---
id: "api-reference/track-refund"
type: "doc"
url: "https://affitor.com/docs/api-reference/track-refund"
kind: "reference"
updated: "2026-10-15"
---
# Track Refund

> Reverse a commission when a sale is refunded

`POST /api/v1/track/refund`

Reverses the affiliate commission tied to a previously tracked sale. Send one call per refund. Each call's `refund_amount_cents` is added to what was already refunded on the sale (capped at the sale amount), and the commission is reduced proportionally: `refunded` while part of the sale remains, `reversed` once all of it is refunded. Omitting `refund_amount_cents` refunds everything that is left. This is the non-Stripe counterpart to the Stripe refund webhook — both use the same underlying reversal logic.

---

## Authentication

All requests must include your program API key as a Bearer token.

```http
Authorization: Bearer YOUR_PROGRAM_API_KEY
Content-Type: application/json
```

The key is scoped to a single program. The refund lookup is restricted to sales that belong to the authenticated program.

---

## Request

### Fields

<ParamList>
  <ParamField name="transaction_id" type="string" required>
    The `transaction_id` originally passed to `POST /api/v1/track/sale`. Used to locate the sale record.
  </ParamField>
  <ParamField name="refund_amount_cents" type="integer">
    This refund's amount in the same unit as the sale's `amount_cents` — not the running total refunded so far. Omit or set to `0` to refund the rest of the sale. Must be a positive integer when provided.
  </ParamField>
  <ParamField name="refund_id" type="string">
    Your own id for this refund (for example the Stripe refund id). Recommended. A refund whose `refund_id` was already applied is ignored, so retries are safe. Without it, a second call with the same amount for the same sale is treated as a retry and ignored. Ignored when the amount is omitted or `0` (refunding the rest is already safe to retry).
  </ParamField>
  <ParamField name="refund_reason" type="string">
    Human-readable reason for the refund. Recorded on the commission and included in partner/advertiser notifications.
  </ParamField>
  <ParamField name="reason" type="string" enum={["refund", "chargeback"]}>
    `refund` or `chargeback`. Recorded as the reason when `refund_reason` is not sent or empty.
  </ParamField>
</ParamList>

### Example request body

<CodeGroup items={[
  {
    label: "cURL",
    lang: "bash",
    code: `curl -X POST https://api.affitor.com/api/v1/track/refund \\
  -H "Authorization: Bearer YOUR_PROGRAM_API_KEY" \\
  -H "Content-Type: application/json" \\
  -d '{
    "transaction_id": "txn_abc123",
    "refund_amount_cents": 5000,
    "refund_id": "re_123",
    "refund_reason": "Customer requested cancellation"
  }'`
  },
  {
    label: "Full refund",
    lang: "json",
    code: `{
  "transaction_id": "txn_abc123"
}`
  },
  {
    label: "Partial refund",
    lang: "json",
    code: `{
  "transaction_id": "txn_abc123",
  "refund_amount_cents": 5000,
  "refund_id": "re_123",
  "refund_reason": "Customer requested cancellation"
}`
  },
]} />

### Multiple refunds and retries

- Two partial refunds of $30 and $20 on a $100 sale: send `3000` then `2000` (each with its own `refund_id`). The commission is reduced by 50% in total.
- Retries: resending a call with the same `refund_id` returns `status: "skipped"` and changes nothing. Without `refund_id`, the amount is used to detect the retry.

---

## Response

### Success — commission reversed or refunded

```json
{
  "success": true,
  "status": "reversed",
  "deductionAmount": 25.00,
  "commission_id": 88
}
```

| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `true` on a 200 response. |
| `status` | string | `"reversed"` (sale fully refunded), `"refunded"` (partially refunded), or `"skipped"` (refund already applied, or commission already reversed). |
| `deductionAmount` | number | Commission amount reversed by this call, in the commission's currency. Taken from the partner's balance only when the commission was already credited (past `pending`, not a test). Present when `status` is `reversed` or `refunded`. |
| `commission_id` | integer | ID of the affected commission record. |

### Success — sale has no commission

```json
{
  "success": true,
  "status": "no_commission",
  "message": "Sale has no commission to reverse"
}
```

### Already-processed refund (idempotent)

```json
{
  "success": true,
  "status": "skipped",
  "reason": "already reversed",
  "commission_id": 88
}
```

---

## Errors

Errors use the [platform envelope](/docs/api-reference/errors#the-platform-envelope) with a stable `error.code`, except a retired API key and rate limiting, which use the [typed envelope](/docs/api-reference/errors#the-typed-envelope).

| Status | `error.code` | When |
|--------|--------------|------|
| 400 | `invalid_request` | `transaction_id` is missing, or `reason` is not `refund` or `chargeback`. |
| 401 | `unauthorized` | `Authorization` header is missing, malformed, or the API token is invalid. |
| 404 | `not_found` | No sale record found for the given `transaction_id` in the authenticated program. |
| 500 | `internal_error` | Internal error during commission reversal. |

---

## Side Effects

When a commission is reversed or refunded, Affitor:

- transitions the commission to `reversed` or `refunded` status
- if the commission was already credited to the partner (any status past `pending`, and not a test), deducts the proportional amount from the partner's balance in the same transaction as the status change; the balance can go negative, and open withdrawals are reduced to fit
- sends an in-app notification to the partner
- sends a manual-review warning to the advertiser if the commission had already been paid out

The status change and the deduction are applied before the response. The notifications are sent afterwards and never change the response.

---

## Related

- [Track Sale](/docs/api-reference/track-sale) — record the original sale
- [Payment Tracking](/docs/brand/tracking/payment-tracking-stripe/) — Stripe webhook alternative
