---
id: "api-reference/track-refund"
type: "doc"
url: "https://affitor.com/docs/api-reference/track-refund"
kind: "reference"
updated: "2026-10-17"
---
# 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). Once all of the sale is refunded, Affitor reverses the commission (`reversed`). A partial refund reduces a commission already approved and cancels one still `pending` (in its hold period, not yet approved); both end `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`). A commission already approved is reduced by 50% in total; one still `pending` (in its hold period, not yet approved) is cancelled by the first refund.
- 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 | The commission share this refund covers, in the commission's currency: the refunded fraction of the commission, or all of what is left once the sale is fully refunded. Taken from the partner's balance only when the commission was already credited (past `pending`, not a test). A `pending` commission that is partly refunded is cancelled whole, so for it this number is informational. 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
}
```

### Test API key

```json
{
  "success": true,
  "test": true
}
```

A request with a test API key gets this answer before the body is checked or the sale is looked up. Nothing is refunded, so a test key cannot reverse a real commission.

---

## 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
