Reference

How to refund a line item in Stripe

A Stripe refund is an amount against a charge or PaymentIntent. Nothing on it says which items the money covers, and there is no line item parameter to pass. Line-item behavior exists in two places: a credit note when an invoice sits behind the payment, and arithmetic you do yourself everywhere else. Both are below, with the tax and discount math and the metadata that keeps your accounting system in the loop.

Verified against official documentation · last reviewed 2026-08-08 · corrections: support@withflintpay.com

The entire targeting surface of a Stripe refund is a payment reference and an amount:

Bash
curl https://api.stripe.com/v1/refunds \
  -u "sk_test_...:" \
  -d payment_intent=pi_... \
  -d amount=1437

Send it and 14.37 moves back to the buyer. Whether that was one defective mug out of a six-item order or a goodwill gesture is recorded nowhere Stripe can see, which is why everything downstream of the refund, from the Dashboard to your accounting connector, shows an unallocated amount. The ask is not new: when a developer requested item-level credits on Stripe's api-discuss list, the answer was that negative line items are not possible and to fake the credit with a coupon amount_off (December 2020; the thread is in the sources). The refund API has not gained a line item parameter since.

Which path applies to you

What you haveYour path
The payment came from a subscription or a Stripe invoiceIssue a credit note
The payment came from Checkout, a Payment Link, or a plain PaymentIntentCompute the amount yourself
One unit of a multi-unit line is coming backPer-unit proration
The item was discounted, or tax was collected on itThe math traps
Accounting needs to know which items a refund coveredKeep line detail attached
You are choosing a payments API and want refunds to target itemsLine-item refunds on Flint

If an invoice sits behind the payment: issue a credit note

Credit notes are the one place Stripe does line-level refunds, and they attach to invoices only. Subscriptions and Stripe Invoicing always create an invoice. Payment-mode Checkout and Payment Links create one only when invoice_creation is enabled. A plain PaymentIntent never has one. The PaymentIntent object no longer carries an invoice field on current API versions, so check by listing invoice payments:

Bash
# An empty list means no invoice: credit notes are not available
curl -G https://api.stripe.com/v1/invoice_payments \
  -u "sk_test_...:" \
  -d "payment[type]=payment_intent" \
  -d "payment[payment_intent]=pi_..."

An empty data array closes this path; skip to computing the amount. A hit gives you the invoice ID, and from there the credit note does what this page's title asks: reference the invoice line item and a quantity, and Stripe works out that line's share of tax and discounts. refund_amount sends the credited amount back to the original payment method:

Bash
curl https://api.stripe.com/v1/credit_notes \
  -u "sk_test_...:" \
  -d invoice=in_... \
  -d "lines[0][type]=invoice_line_item" \
  -d "lines[0][invoice_line_item]=il_..." \
  -d "lines[0][quantity]=1" \
  -d refund_amount=1437

You can link refunds you created separately through refunds[] instead. Credit notes stay inside the invoice's arithmetic: combined credit notes and refunds cannot exceed what the invoice charged.

Everywhere else: compute the amount yourself

The recipe: read what the line actually settled for, split it per unit without losing cents, refund that number, and write down what it was for. For Checkout, the session's line items carry the settled amounts, discounts and tax included. Payment Link payments create Checkout Sessions too, so the same call covers them:

JavaScript
const { data: lineItems } = await stripe.checkout.sessions.listLineItems(
  sessionId,
  { limit: 100 },
);

// Each line item reports what the buyer actually paid for it:
// amount_subtotal, amount_discount, amount_tax, amount_total, quantity
const line = lineItems.find((li) => li.price.product === productId);

amount_total is what the buyer actually paid for the line after discounts, with tax inside or on top depending on the price's tax_behavior. Split it per unit once and keep the array:

JavaScript
// Split a line's settled total into per-unit amounts, in integer minor
// units, so that repeated partial refunds sum exactly to the total.
function perUnitAmounts(amountTotal, quantity) {
  const base = Math.floor(amountTotal / quantity);
  const extra = amountTotal - base * quantity;
  return Array.from({ length: quantity }, (_, i) =>
    i < extra ? base + 1 : base,
  );
}

// A 3-unit line that settled at 4310 with tax and discounts in it:
perUnitAmounts(4310, 3); // [1437, 1437, 1436]

Then refund the unit's amount, and make the refund carry its own explanation:

JavaScript
const refund = await stripe.refunds.create(
  {
    payment_intent: paymentIntentId,
    amount: 1437,
    metadata: {
      // Fixed keys your accounting integration reads back from the
      // webhook events that carry this refund. Stripe never reads them.
      line_item_id: line.id,
      product_id: line.price.product,
      units_refunded: "1",
    },
  },
  { idempotencyKey: `refund-${orderId}-${line.id}-unit-1` },
);

Stripe checks this refund against the charge's remaining balance and nothing else. It will process a second refund for the same unit as long as the charge has balance left, so per-line bookkeeping is yours; the traps below are where that bookkeeping goes wrong. For a plain PaymentIntent with no session, the same recipe applies, with the line amounts coming from your own order data, because Stripe never saw the items.

The four places the math goes wrong

01

A cart discount means the sticker price is wrong

The buyer used a promotion code, and refunding quantity * unit_amount gives back more than they paid for the item.

Checkout spreads a cart-level discount across the lines it applies to. The line's amount_total already reflects its share of the discount; the price object's unit_amount does not. The reverse shortcut fails too: a coupon with amount_off, the stand-in Stripe has suggested for item-level credits, discounts a future payment. It refunds nothing on this one, so it only fits a buyer who is about to pay you again.

Confirm it

Compare amount_subtotal with amount_total on the session line item. If they differ, a discount or tax rule touched this line and unit_amount arithmetic is off the table.

Fix

Base every refund calculation on amount_total and amount_discount from the session's line items, never on the price. The proration helper above does this per unit.

02

The refund does not know about tax

You refunded the item's price, and the buyer is still out the tax they paid on it, or your tax records still show the full sale.

The amount you send is the only money that moves. Stripe does not add the item's tax share to a partial refund, and whether tax sits inside or on top of amount_total depends on the price's tax_behavior. Refunding money also never edits a tax record: if you record tax with the Tax Transactions API, the transaction stays at the full sale amount until you record a reversal yourself.

Confirm it

amount_tax on the session line item says how much tax the line carried. If your refund amount ignores it, the buyer got a net-of-tax refund.

Fix

Refund the unit's share of amount_total, which includes its tax. Then adjust the tax side where you track it: a reversal in the Tax Transactions API, or your own filing records.

03

Per-unit division loses cents

Refunding the last unit of a line fails because the charge's remaining balance is a cent short, or your refunds sum to a cent more than the line total.

A line's total is rarely divisible by its quantity once discounts and tax are in it. Rounding amount_total / quantity fresh on each refund drifts by a cent per partial, and after a few of them your arithmetic and the charge disagree.

Confirm it

Multiply your per-unit amount by the quantity and compare it with amount_total. Any difference is drift waiting to surface.

Fix

Precompute every unit's amount once with a largest-remainder split, store the array, and consume it as units come back. The parts sum to the total by construction.

04

Nothing stops a second refund of the same item

An item was refunded twice, and Stripe accepted both requests without complaint.

Stripe validates a refund against the charge's remaining refundable balance, not against which item you meant it to cover. Two support agents refunding the same item hit no guardrail until the whole charge is exhausted.

Confirm it

Sum the refunds you have attributed to a line and compare with that line's amount_total. Stripe has no view that answers this for you.

Fix

Record per-line refunded amounts in your own system when you create each refund, and check them before creating the next one. The metadata convention below makes each refund carry the evidence.

Getting line detail into your accounting system

Accounting integrations build credit memos from data on the refund. Stripe's refund object gives them an amount, a payment reference, and nothing else. That is why NetSuite, ERP, and OMS connectors show Stripe refunds as unallocated amounts against an order, and why credit memos come out as one-line adjustments instead of mirroring the items that came back. The connector is not broken; there is nothing for it to read.

Two ways to give it something to read:

  • The credit note, when there is an invoice. It is itself the line-level record, and connectors that understand Stripe invoicing can mirror its lines into a credit memo directly.
  • Metadata on the refund, everywhere else. Pick a fixed convention once and write it at create time, as in the sample above: the line item ID or SKU, units, and amounts. Metadata holds up to 50 keys with 40-character names and 500-character values, enough for a compact JSON value covering several items, and it rides along in the webhook events that carry the refund, so the connector can allocate without another API call.

Neither fixes history. A refund created without context stays unallocated, so if reconciliation matters, put the convention in place before the next refund rather than after.

Questions, answered

Can you refund a single line item in Stripe?

Not directly. A Stripe refund is an amount against a charge or PaymentIntent and carries no reference to items. If the payment has an invoice behind it, a credit note can credit specific invoice lines and pay the money back. Everywhere else you compute the item's share of the payment yourself and refund that amount.

Can I use a credit note to refund a Stripe Checkout payment?

Only when the session produced an invoice. Subscription-mode sessions always do. Payment-mode Checkout and Payment Links produce one only when invoice creation is enabled, and a plain PaymentIntent never has one, so there is nothing for a credit note to attach to. For those payments, compute the amount yourself.

Does Stripe calculate the tax share when I refund part of a payment?

No. The amount you send is the amount that moves. For a partial refund on Checkout or a PaymentIntent you include the item's tax share in the amount yourself, and any tax transaction you recorded needs its own reversal. Credit notes are the exception: on an invoice, the credited lines carry their tax with them.

How do I record which items a Stripe refund covered?

Write it on the refund's metadata at create time with a fixed set of keys, and read it back from the webhook events that carry the refund. When the payment has an invoice, issue a credit note instead; the credit note itself is the line-level record.

Why doesn't my accounting integration show which items were refunded?

Because the refund object has no line items to read. Connectors that build credit memos can only allocate a refund when something carries the line detail: a credit note on an invoice, or metadata your integration wrote on the refund when it was created.

Line-item refunds on Flint

Flint refunds are order-aware, so the request names the items. Pass the order's line item ID and a quantity, and the server computes the amount from what that line actually settled for, its discount share and tax included:

Bash
curl -X POST https://api.withflintpay.com/v1/refunds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: refund-ord-101" \
  -d '{
    "order_id": "ord_1kmn0aExample",
    "reason": "requested_by_customer",
    "line_items": [
      {"order_line_item_id": "li_1kmn0aExample", "quantity": 1}
    ]
  }'

The response itemizes the arithmetic per line in line_item_allocations: the subtotal, the discount share, the tax that reversed, and the total that moved:

JSON
"line_item_allocations": [
  {
    "order_line_item_id": "li_1kmn0aExample",
    "quantity": 1,
    "refunded_money": {"amount": 1437, "currency": "USD"},
    "automatic_refund": {
      "subtotal_money": {"amount": 1499, "currency": "USD"},
      "discount_money": {"amount": 150, "currency": "USD"},
      "tax_money": {"amount": 88, "currency": "USD"},
      "total_money": {"amount": 1437, "currency": "USD"}
    },
    "tax_refund_mode": "automatic"
  }
]

Everything this page has you build by hand is the server's job here. Repeated partial refunds draw down each line's remaining balance, refunding a line past what it settled for fails with a specific error instead of silently succeeding, and the rounding is deterministic integer math. Tax reverses automatically from the settled amounts, with an explicit override for the cases that need to reverse less. Restocking fees and withheld amounts are adjustments on the same request:

Bash
curl -X POST https://api.withflintpay.com/v1/refunds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: refund-ord-102" \
  -d '{
    "order_id": "ord_1kmn0aExample",
    "reason": "customer_changed_mind",
    "line_items": [
      {
        "order_line_item_id": "li_1kmn0aExample",
        "quantity": 1,
        "refund_adjustments": [
          {
            "adjustment_type": "restocking_fee",
            "applies_to": "none",
            "amount_money": {"amount": 500, "currency": "USD"},
            "reason": {"code": "restocking_policy"}
          }
        ]
      }
    ]
  }'
Stripe, card paymentsStripe, invoice paymentsFlint
Refund one unit of an itemYou compute the amountCredit note line with a quantityline_items with a quantity; the server computes
Tax on the refunded unitYour math, plus a separate tax reversal if you record taxCarried by the credit noteReversed automatically from what the line settled for
Discounted itemsYou prorate the cart discountProrated on the invoiceAllocated server-side, itemized in the response
Refunding past the item's shareAllowed; only the charge total is checkedCapped by the invoiceFails per line with a specific error
Where the line detail lives afterwardMetadata you wroteThe credit noteThe refund's per-line allocations and the order itself

The refunds guide covers the full lifecycle, including the failure cases, and the Refunds API reference has every field on the requests above. For why order structure is what makes this possible, see Partial refunds start with order structure.

Sources