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

idstring
Prefixed pay_.
amountinteger
In the smallest currency unit. Includes tax: the amount charged, whichever tax_behavior it was sent with.
subtotalinteger
amount less tax.
taxinteger
Tax inside 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.
tax_behaviorenum
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.
line_itemsarray | absent
What the payment is for, in the order you sent them. Empty when the payment has none, and absent on list unless you pass expand[]=line_items, so narrow before you read it. subtotal and tax are always present either way. See the line item object.
tax_rateinteger | null | absent
The rate the whole amount carries when the payment has no line items; null when the lines carry their own. Present together with line_items.
tax_breakdownarray | absent
Tax by rate, highest first: tax_rate, taxable_amount (the net taxed at that rate) and tax_amount. Present together with line_items.
currencystring
statusenum
created, processing, succeeded, failed or refunded. canceled is reserved and not set by any flow today.
customerstring | null
Customer the payment belongs to.
descriptionstring | null
external_referencestring | null
Your reference, from the session or payment create call.
metadataobject
collection_methodenum | null
card, wallet or bank_invoice once known.
refunded_amountinteger
Total refunded so far.
failure_reasonstring | null
Processor's reason when status is failed.
card_brandstring | null
For example visa or mc.
card_last4string | null
wallet_typestring | null
apple_pay or google_pay when a wallet was used.
created_attimestamp
updated_attimestamp

Create a payment

POST/v1/paymentsSecret key

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

Idempotency-Keystring
Makes the call safe to retry; see Idempotency. Optional for a plain payment, required when payment_method is set.

Body

amountinteger
1 to 2,000,000,000 in the smallest currency unit. Required unless you send line_items, which derive it.
line_itemsarray
1 to 100 lines. See line items.
tax_rateinteger
VAT inside 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.
tax_behaviorenumdefault: inclusive
For a payment without 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.
currencystringdefault: ISK
customerstring
Must belong to your merchant in this mode. With payment_method, it is taken from the card and only needs to be sent if you want it checked.
payment_methodstring
A saved card (pm_…) to charge off-session. Requires an Idempotency-Key header. See charging a saved card.
descriptionstring
external_referencestring
metadataobject
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.

Node.jsts
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", and payment.succeeded is sent.
  • Declined: the request fails with a payment_error (402): card_declined, insufficient_funds, expired_card, incorrect_cvc or processing_error. The message names the reason, the payment exists in status failed with the same failure_reason, and payment.failed is 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

idstring
Prefixed pli_.
descriptionstring
1 to 500 characters. Printed on the invoice line.
quantityinteger
1 to 1,000,000.
unit_amountinteger
Price per unit including tax, in the smallest currency unit: the price the payer sees. Negative on a discount line. Send the net instead with tax_behavior: "exclusive"; the response always returns the price including tax here.
unit_amount_excluding_taxinteger
Price per unit excluding tax, in the response: the unit_amount you sent on an exclusive line.
tax_behaviorenumdefault: inclusive
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.
tax_rateinteger
VAT inside 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.
tax_amountinteger
Tax inside this line, computed by Borga.
amountinteger
Line total: unit_amount * quantity.
accounting_codestring | null
GL account code for the line.
metadataobject
Stored and returned. Never shown to the payer.

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.

line-items-session.tsts
// 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

GET/v1/payments/:idSecret key
Node.jsts
const payment = await borga.payments.retrieve("pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6");

console.log(payment.status); // "succeeded"
console.log(payment.card_brand, payment.card_last4); // "visa" "1142"
Response
{
  "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

GET/v1/paymentsSecret key

Query parameters

statusenum
Filter by payment status.
expandarray
Pass 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.
limitintegerdefault: 25
Up to 100.
starting_afterstring
Cursor; see Pagination.
curl "https://api.borga.is/v1/payments?status=succeeded&limit=50" \
  -H "Authorization: Bearer sk_test_…"