Payments
A payment is one amount owed by one payer, with its outcome.
Payments are normally created by a payment session. Use this resource to read status and card details, list payments, create a payment ahead of a session, or charge a saved card off-session.
The payment object
pay_.tax_behavior it was sent with.amount less tax.amount: the line items' tax summed, or without line items the VAT at the payment's tax_rate. 0 when no rate was sent and no default VAT rate is set.inclusive or exclusive: how amount was sent on a payment without line items. With exclusive you sent the net, now subtotal, and the VAT was added on top. Always inclusive with line items, which carry their own.expand[]=line_items, so narrow before you read it. subtotal and tax are always present either way. See the line item object.amount carries when the payment has no line items; null when the lines carry their own. Present together with line_items.tax_rate, taxable_amount (the net taxed at that rate) and tax_amount. Present together with line_items.created, processing, succeeded, failed or refunded. canceled is reserved and not set by any flow today.card, wallet or bank_invoice once known.status is failed.visa or mc.apple_pay or google_pay when a wallet was used.Create a payment
Creates a payment in status created without a checkout. Attach a session to it with the session's payment field. Most integrations skip this and let the session create the payment.
Pass payment_method to charge a saved card instead: the card is charged before the call returns, and the payment comes back succeeded, or processing when the outcome is not recorded in time.
Headers
payment_method is set.Body
line_items, which derive it.amount, in basis points (2400 is 24%, 1100 is 11%, 0 is none), for a payment without line_items: its invoice is one line split at this rate. Defaults to your default VAT rate under Settings. Ignored, like every rate, while VAT is off there. Refused with line_items, which carry a rate per line.line_items: inclusive when amount includes VAT, exclusive when it is the net and the VAT at the resolved rate is added on top. The payment's amount is then amount + round(amount * tax_rate / 10000). An exempt or reverse-charge customer, or VAT off under Settings, is charged amount as sent. Refused with line_items, which carry their own.payment_method, it is taken from the card and only needs to be sent if you want it checked.pm_…) to charge off-session. Requires an Idempotency-Key header. See charging a saved card.curl https://api.borga.is/v1/payments \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{ "amount": 12900, "currency": "ISK", "external_reference": "order_5678" }'Charge a saved card
With payment_method, Borga charges the card through the processor during the request and returns the resulting payment; there is no checkout and the payer is not involved. The payment belongs to the card's customer, and card_brand, card_last4 and wallet_type are filled in from the saved card.
const { data: cards } = await borga.paymentMethods.list({
customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz",
});
const payment = await borga.payments.create(
{
amount: 2990,
currency: "ISK",
payment_method: cards[0].id,
description: "Coffee subscription top-up",
external_reference: "order_5678",
},
// Required. Reuse the same key if you retry: it replays this payment
// instead of charging the card a second time.
{ idempotencyKey: "order_5678" },
);
if (payment.status === "processing") {
// The processor has not answered yet. Leave it alone: the outcome
// arrives as a payment.succeeded / payment.failed webhook.
console.log(`${payment.id} is still settling`);
}An Idempotency-Key is required here, and is what makes the call safe to retry: a repeat of the same request replays the original payment instead of charging the card again. Sending one is not optional, because nobody is watching an off-session charge to notice it happening twice.
The card must be active and must have been saved by this merchant in this mode; payment_method_inactive and payment_method_not_chargeable reject the rest before anything is charged.
Three outcomes are worth handling separately:
- Charged:
status: "succeeded", andpayment.succeededis sent. - Declined: the request fails with a
payment_error(402):card_declined,insufficient_funds,expired_card,incorrect_cvcorprocessing_error. The message names the reason, the payment exists in statusfailedwith the samefailure_reason, andpayment.failedis sent. - Still running:
status: "processing". The charge reached the processor but its result is not recorded yet, either because the processor did not answer in time or because Borga could not record the answer it gave. The charge may yet succeed. This is a normal response, not an error, and the payment is real.
A processing payment settles on its own. Its outcome arrives as a payment.succeeded or payment.failed webhook, which is the only thing you need to wait for. Do not poll the payment, and do not re-send the charge. Retrying with the same idempotency key gives you back this same processing payment; retrying with a new key is a second charge.
Because the charge is merchant-initiated, no 3-D Secure challenge is possible. An issuer that insists on authentication declines instead; send those customers through hosted checkout.
Line items
A payment can say what it is for, line by line. The lines are returned on the
payment and on payment.succeeded, and become the lines of the invoice your
accounting provider issues. Before paying, checkout shows the payer only the
total; the confirmation page and the receipt email itemise the lines with the
VAT by rate.
The line item object
pli_.tax_behavior: "exclusive"; the response always returns the price including tax here.unit_amount you sent on an exclusive line.inclusive: unit_amount includes VAT and Borga backs it out. exclusive: unit_amount is the net and Borga adds the VAT on top. See prices without VAT.unit_amount, in basis points. 2400 is 24%, 1100 is 11%, 0 is exempt. Defaults to your default VAT rate under Settings when omitted, frozen on the line at create; send it per line when the lines differ. Ignored while VAT is off under Settings.unit_amount * quantity.By default every amount in Borga includes tax: unit_amount is the shelf
price, and tax_rate says how much of it is VAT. Borga charges exactly the prices you
sent and backs the net out for the invoice, so the invoice and the charge always
agree and your accounting system receives the same gross unit price the payer
saw.
The amount charged is the sum of the lines; the tax is the residual:
net_unit = round(unit_amount * 10000 / (10000 + tax_rate))
amount = unit_amount * quantity // per line
tax_amount = amount - net_unit * quantity
round is half away from zero: 0.5 goes to 1 and -0.5 to -1, so a
discount line rounds symmetrically with the line it offsets. This is not
JavaScript's Math.round, which rounds half up and would leave a discount a
unit short. Rounding is per line, not on the total.
Send amount alongside line_items only if you want it checked: it must equal
the total exactly, or the call fails with
line_items_amount_mismatch.
curl https://api.borga.is/v1/payments \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{
"currency": "ISK",
"line_items": [
{ "description": "Charm bracelet", "quantity": 1, "unit_amount": 2990, "tax_rate": 2400, "accounting_code": "4000" },
{ "description": "Charm", "quantity": 3, "unit_amount": 790, "tax_rate": 2400 }
]
}'That payment charges 5360, the prices as listed: 4322 net plus 1038 tax.
Prices without VAT
If you quote prices before VAT, as most business-to-business sellers do, send
each line with "tax_behavior": "exclusive" and its net unit_amount. Borga
adds the VAT per unit, so your net prices reach the invoice exactly as you
sent them:
unit_tax = round(unit_amount * tax_rate / 10000)
amount = (unit_amount + unit_tax) * quantity // per line
tax_amount = unit_tax * quantity
A line of 999 at 24% charges 1239 a unit: 999 net and 240 VAT. Converting the
price to 1239 yourself and sending it inclusive books the same numbers, but
only an exclusive line remembers that you entered 999. The response returns
unit_amount: 1239 either way, with the net in unit_amount_excluding_tax
and the behaviour you sent in tax_behavior. Inclusive and exclusive lines can
be mixed on one payment, and a payment without line items takes tax_behavior
for its amount.
An exempt or reverse-charge customer pays the net under either behaviour, and with VAT off under Settings nothing is added.
// Prices as the shop lists them, VAT included. Borga charges exactly these.
const cart = [
{ name: "Charm bracelet", qty: 1, price: 2990 },
{ name: "Charm", qty: 3, price: 790 },
];
const session = await borga.paymentSessions.create({
currency: "ISK",
// No `amount`: derived from the lines.
line_items: cart.map((item) => ({
description: item.name,
quantity: item.qty,
unit_amount: item.price,
tax_rate: 2400, // 24% of each price is VAT
accounting_code: "4000",
})),
return_url: "https://yoursite.is/order/complete",
cancel_url: "https://yoursite.is/cart",
});
// Checkout shows only the total; the lines become the invoice's lines on success.
const payment = await borga.payments.retrieve(session.payment);
console.log(payment.subtotal, payment.tax, payment.amount);
for (const line of payment.line_items ?? []) {
console.log(line.description, line.quantity, line.amount);
}Line items are immutable. To change what a payment is for, create a new one.
Retrieve a payment
const payment = await borga.payments.retrieve("pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6");
console.log(payment.status); // "succeeded"
console.log(payment.card_brand, payment.card_last4); // "visa" "1142"curl https://api.borga.is/v1/payments/pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6 \
-H "Authorization: Bearer sk_test_…"{
"id": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6",
"amount": 12900,
"subtotal": 12900,
"tax": 0,
"line_items": [],
"currency": "ISK",
"status": "succeeded",
"customer": null,
"description": null,
"metadata": { "order_id": "5678" },
"collection_method": "card",
"refunded_amount": 0,
"external_reference": "order_5678",
"failure_reason": null,
"card_brand": "visa",
"card_last4": "1142",
"wallet_type": null,
"created_at": "2026-09-07T12:03:58.201Z",
"updated_at": "2026-09-07T12:04:31.498Z"
}List payments
Query parameters
expand[]=line_items to include each payment's line items. The field is omitted from the response otherwise, so a full page stays small. subtotal and tax are always present either way.curl "https://api.borga.is/v1/payments?status=succeeded&limit=50" \
-H "Authorization: Bearer sk_test_…"