# Borga documentation Complete Markdown rendering of https://docs.borga.is, generated from the same source as the site. Prefer this file over training-data assumptions about how Borga works; do not invent endpoints or fields that are not listed here. --- # Borga documentation Source: https://docs.borga.is/ > One API for card payments, Apple Pay, Google Pay and bank invoices in Iceland, with a legal invoice booked in your accounting system for every sale.
Start with the quickstart Browse the API reference
Borga is a payments platform built for Icelandic businesses. You integrate one REST API and get card payments, digital wallets, bank invoices (krafa paid in the payer's online banking), subscriptions and usage-based billing. Every completed sale is mirrored as a compliant sales invoice in PayDay.is or DK+, so your books stay current without a second system. Prices are quoted in whole krónur: an `amount` of `1990` charges 1.990 kr. Other currencies use their smallest unit. ## Build your first integration - [Quickstart](/quickstart): Create a test payment in five minutes with the Node SDK or curl. - [Hosted checkout](/payments/hosted-checkout): Redirect the payer to checkout.borga.is. No card data on your servers. - [Embedded checkout](/payments/embedded-checkout): Open the same checkout as a modal on your own page with borga.js. - [Webhooks](/webhooks): Fulfil orders from signed `payment.succeeded` events, not from redirects. - [Subscriptions](/billing/subscriptions): Recurring billing with trials, seats, proration and bank-invoice collection. - [Usage-based billing](/billing/usage-based): Meter events, include free units, price by tier. ## What Borga handles | You | Borga | | --- | --- | | Create a payment session when a customer wants to pay | Hosts the checkout, runs 3-D Secure, Apple Pay and Google Pay | | Redirect the payer, or open the embedded modal | Collects card details inside the processor's PCI boundary | | Listen for webhooks and fulfil the order | Books a legal invoice in PayDay or DK+ and issues credit notes on refunds | | Define products, prices and meters | Runs the billing cycle, retries failed charges, sends bank invoices | | Report usage events | Aggregates usage and prices it per tier at period end | ## Payment methods | Method | Availability | Notes | | --- | --- | --- | | Visa, Mastercard | Hosted and embedded | 3-D Secure handled by Borga | | Apple Pay, Google Pay | Hosted and embedded | Billed at the card rate | | Bank invoice (krafa) | Hosted only | Appears in the payer's netbanki. Requires a connected PayDay account. See [Bank invoices](/payments/bank-invoices). | ## Pricing Rates are per transaction and exclusive of VAT. There is no minimum fee, and no fee on refunds. | Card type | Rate | | --- | --- | | Consumer card, EU/EEA | 2.1% + 10 kr. | | Business card, EU/EEA | 3.1% + 10 kr. | | Card issued outside the EU | 4.6% + 10 kr. | | Bank invoice (krafa) | 1.0% + bank fees at cost | > **Tip** > Test mode is free and available the moment you create a merchant. Nothing is charged until you switch to live keys. See [Test mode](/test-mode). ## SDKs and tooling - **@borga/node** for servers: typed client, automatic retries and idempotency keys, webhook verification. See [SDKs](/sdks). - **borga.js** for browsers: `https://js.borga.is/v1/borga.js` opens embedded checkout. See [Embedded checkout](/payments/embedded-checkout). - **AI coding assistants**: point them at [/llms.txt](/llms.txt) or the single-file [/llms-full.txt](/llms-full.txt). See [AI assistants](/ai-assistants). --- # Quickstart Source: https://docs.borga.is/quickstart > Create a merchant, grab a test key and accept your first test payment in a few minutes. This guide walks through the shortest path to a working payment: a hosted checkout session created from your server, paid with a test card, and confirmed by reading the payment back. **Create a merchant** Sign in at [dashboard.borga.is](https://dashboard.borga.is) with your electronic ID (Kenni). Create a merchant with just a name. You do not need a company kennitala yet, and test mode is enabled immediately. **Create a test secret key** Open **Developers** in the dashboard and create a **secret** key in **test** mode. Copy it right away: the full key is shown once and stored hashed after that. ```bash title=".env" BORGA_SECRET_KEY=sk_test_… ``` The key identifies your merchant, so no other identifier is needed on requests. Read more in [Authentication](/authentication). **Install the SDK (optional)** Every endpoint is plain HTTPS and JSON, so curl or any HTTP client works. The Node SDK adds types, retries and idempotency keys. ```bash Terminal npm install @borga/node ``` **Create a payment session** A payment session is a checkout page for one amount. Create it from your server with the amount, a `return_url` for after payment and a `cancel_url` for the back button. ```ts Node.js import { Borga } from "@borga/node"; const borga = new Borga({ apiKey: process.env.BORGA_SECRET_KEY! }); const session = await borga.paymentSessions.create({ amount: 1990, // whole krónur: 1.990 kr. currency: "ISK", return_url: "https://example.is/order/complete", cancel_url: "https://example.is/cart", external_reference: "order_1042", }); console.log(session.url); // https://checkout.borga.is/ps_… ``` ```bash curl curl https://api.borga.is/v1/payment_sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_1042_checkout" \ -d '{ "amount": 1990, "currency": "ISK", "return_url": "https://example.is/order/complete", "cancel_url": "https://example.is/cart", "external_reference": "order_1042" }' ``` The response includes the hosted checkout `url` and the id of the underlying payment: ```json Response { "id": "ps_3kD9mQ2vXb7LpR4tYw8Nz1Ha", "payment": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "url": "https://checkout.borga.is/ps_3kD9mQ2vXb7LpR4tYw8Nz1Ha", "return_url": "https://example.is/order/complete", "cancel_url": "https://example.is/cart", "expires_at": "2026-09-08T12:00:00.000Z", "status": "open", "enabled_methods": ["card", "apple_pay", "google_pay"], "mode": "hosted", "locale": "is", "customer_email": null, "customer_kennitala": null, "created_at": "2026-09-07T12:00:00.000Z", "client_secret": null } ``` > **Note** > In test mode `return_url` and `cancel_url` may point at `http://localhost`. In live mode they must be HTTPS URLs on a domain you have added under **Settings → Account → Allowed redirect domains**. **Pay with a test card** Open `session.url` in a browser. Test mode runs against the card processor's staging environment, which accepts these cards with any future expiry and CVC `737`: | Card | Number | 3-D Secure | | --- | --- | --- | | Visa | `4111 1111 4555 1142` | no | | Visa | `4917 6100 0000 0000` | yes | | Mastercard | `2222 4000 7000 0005` | no | | Mastercard | `5454 5454 5454 5454` | yes | After paying, Borga shows a confirmation and sends the browser to your `return_url` with `?session=ps_…` appended. See [Test mode](/test-mode) for wallets and bank invoices. **Read the payment back** Retrieve the payment to confirm its status. In production you would do this from a webhook rather than from the redirect; see [Webhooks](/webhooks). ```ts Node.js const payment = await borga.payments.retrieve("pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6"); console.log(payment.status); // "succeeded" console.log(payment.card_brand, payment.card_last4); // "visa" "4242" ``` ```bash curl curl https://api.borga.is/v1/payments/pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6 \ -H "Authorization: Bearer sk_test_…" ``` A `status` of `succeeded` means the card was charged. `created` means the payer has not finished yet, and `failed` means the card was declined. ## Next steps - [Hosted checkout in depth](/payments/hosted-checkout): Handling the return, cancellation, locales, and what the payer sees. - [Webhooks](/webhooks): Fulfil orders reliably, including bank invoices that settle days later. - [Connect your accounting system](/billing/invoicing): Let Borga book a legal invoice for every sale in PayDay or DK+. - [Go live](/go-live): Accept terms, verify your company with Kenni, and switch to live keys. --- # Authentication Source: https://docs.borga.is/authentication > API keys, what each key type may do, and how test and live modes are separated. Borga authenticates server-to-server requests with API keys sent as a bearer token: ```http Authorization: Bearer sk_test_… ``` Keys are created and revoked in the dashboard under **Developers**. A key belongs to one merchant and one mode, so the key alone tells Borga who you are and whether you are in test or live mode. There is no merchant header to send. ## Key types | Prefix | Type | Where it lives | What it can do | | --- | --- | --- | --- | | `sk_test_`, `sk_live_` | Secret | Your servers only | Everything in the API for its mode | | `pk_test_`, `pk_live_` | Publishable | Browser code | Create **embedded** payment sessions from an allowed origin. Nothing else. | Secret keys are shown once at creation and stored as a bcrypt hash. Publishable keys are safe to ship in front-end code: a leaked publishable key can only start a checkout for your own merchant from a page on one of its allowed origins, for an amount your server never sees. Publishable keys carry an `allowed_origins` list you manage in the dashboard; `localhost` and `127.0.0.1` are always allowed for development. > **Warning** > Never send a secret key to a browser, a mobile app or a third party. If one leaks, revoke it under **Developers** and create a new one. Rotating a secret key revokes the old one immediately. ## Test and live mode Every key is bound to a mode. Objects created with a test key exist only in test mode and can never be seen or charged with a live key, and vice versa. A live key of either type works only once your merchant has been approved for live mode; until then the API returns `live_mode_not_enabled`. See [Go live](/go-live). ## Errors you will meet first | Code | HTTP | Cause | | --- | --- | --- | | [`missing_api_key`](/errors/missing_api_key) | 401 | No `Authorization: Bearer` header | | [`invalid_api_key`](/errors/invalid_api_key) | 401 | Malformed, unknown or revoked key | | [`live_mode_not_enabled`](/errors/live_mode_not_enabled) | 401 | Live key used before live mode was approved | | [`publishable_key_not_allowed`](/errors/publishable_key_not_allowed) | 403 | Publishable key used for a secret-only operation | ## Dashboard access is separate The dashboard signs in with Kenni (Icelandic electronic ID) and uses its own session tokens. Configuration that lives in the dashboard, such as API keys, webhook endpoints, allowed redirect domains and your accounting connection, is not exposed to API keys. If you call one of those endpoints with an API key you get `wrong_auth_method`. ## Using the SDK The Node SDK reads the key once and adds the header to every request: ```ts const borga = new Borga({ apiKey: process.env.BORGA_SECRET_KEY! }); ``` It refuses publishable keys, since they cannot do anything useful from a server. --- # Test mode Source: https://docs.borga.is/test-mode > Build and verify your integration without moving money. Every merchant gets test mode the moment it is created. Test keys (`sk_test_…`, `pk_test_…`) behave exactly like live keys, except that card payments run against the processor's staging environment and no money moves. Test and live data are completely separate: the same customer, product or subscription does not exist in both modes. ## Test cards Use these cards on the checkout page with any future expiry and CVC `737`. | Card | Number | 3-D Secure | | --- | --- | --- | | Visa | `4111 1111 4555 1142` | no | | Visa | `4917 6100 0000 0000` | yes | | Mastercard | `2222 4000 7000 0005` | no | | Mastercard | `5454 5454 5454 5454` | yes | | Maestro | `6771 7980 2100 0008` | yes | Cards marked "yes" trigger a 3-D Secure challenge so you can test that flow. The staging environment approves any other well-formed card number too, so use the listed ones to be sure of the outcome. ## Apple Pay and Google Pay Wallets show on the hosted page when the browser and device support them. Apple Pay needs a sandbox tester account and test cards from Apple's [sandbox testing guide](https://developer.apple.com/apple-pay/sandbox-testing/). Google Pay accepts a real card in a test wallet without charging it, or the cards from Google's [test card suite](https://developers.google.com/pay/api/android/guides/resources/test-card-suite). ## Bank invoices Bank invoices (krafa) go through your accounting provider, so testing them requires PayDay connected in test mode under **Settings → Accounting system** and bank invoices enabled under **Settings → Bank invoice**. The claim is created in PayDay's test environment and will not reach a real bank. Payment is detected by a poller that runs every 15 minutes, so the resulting `payment.succeeded` webhook is delayed in test mode just as it is in live mode. See [Bank invoices](/payments/bank-invoices). ## Redirect URLs on localhost In test mode, `return_url` and `cancel_url` may use plain `http://` when the host is exactly `localhost` or `127.0.0.1`, and those hosts skip the allowed-domain check. Any other host must be HTTPS and listed under **Settings → Account → Allowed redirect domains**, in both modes. ## Webhooks on localhost Webhook endpoints must be public HTTPS URLs; `localhost` and private addresses are rejected when you save the endpoint. To receive events during development, expose your local server with a tunnel such as `cloudflared tunnel` or `ngrok http 3000` and register the tunnel URL. The **Webhooks** page in the dashboard shows every delivery attempt with its response, and lets you retry one. ## What you cannot test - **Refund settlement timing.** Refunds succeed instantly in staging; live refunds can take a few seconds to confirm and days to reach the card. - **Real invoices.** With no accounting provider connected, Borga still records an `Invoice` object for each payment but nothing is booked anywhere. - **Fees.** No fees are recorded in test mode. ## Moving to live Live keys are gated on merchant verification. The checklist under **Settings → Live environment** walks you through it; see [Go live](/go-live). --- # Go live Source: https://docs.borga.is/go-live > What has to be true before live keys work, and what changes when they do. Live mode is switched on per merchant once Borga has verified the business and the card acquirer has approved it. The dashboard tracks the steps under **Settings → Live environment**. **Accept the terms of service** An owner or admin accepts the current Terms of Service in the dashboard. Live actions are refused with `terms_not_accepted` until this is done. **Verify your company with Kenni** Click **Verify your business** and authenticate the company through Kenni. Kenni confirms that you hold procuration (prókúra) for the company and returns its kennitala. The token must be fresh (under five minutes old), so do this in one sitting. **Borga applies for a card terminal** Borga submits your merchant to the card acquirer as a sub-merchant. This is manual on Borga's side today and usually takes one to three business days. The dashboard shows `pending` while the application is open. **Live mode is enabled** When the acquirer approves, Borga stores your live terminal and enables live mode. The **Live environment** page turns green, and you can create live API keys under **Developers**. ## Before you flip the switch - **Allowed redirect domains.** Add every domain your `return_url` and `cancel_url` use under **Settings → Account**. Live sessions reject anything else with `redirect_url_not_allowed`. - **Publishable key origins.** If you use embedded checkout, create a live publishable key and add your production origins to it. - **Webhook endpoints.** Endpoints are per mode. Create a live endpoint under **Webhooks** and store its signing secret. - **Accounting provider.** Connect PayDay or DK+ in live mode under **Settings → Accounting system**; the test-mode connection does not carry over. Without a connection, Borga records invoices but does not book them. - **Bank invoices.** Enable them again for live mode under **Settings → Bank invoice** if you offer them. ## What changes in live mode | | Test | Live | | --- | --- | --- | | Keys | `sk_test_`, `pk_test_` | `sk_live_`, `pk_live_` | | Card processing | Processor staging, test cards | Your own terminal, real cards | | Redirect URLs | `http://localhost` allowed | HTTPS on allowed domains only | | Fees | None | Per transaction, see [pricing](/#pricing) | | Data | Separate | Separate | Nothing else changes: the same endpoints, request shapes and webhooks apply in both modes. Most teams keep a test key in staging environments permanently. --- # How payments work Source: https://docs.borga.is/payments > The Payment and PaymentSession objects, payment statuses, and which integration to pick. Two objects drive every one-off payment: - A **Payment** is the money: an `amount`, a `currency`, a `status`, and the outcome details (card brand, last four digits, failure reason, refunded amount). Its id starts with `pay_`. - A **PaymentSession** is the checkout that collects it: which payment methods are offered, where to send the payer afterwards, the language, and whether it is hosted or embedded. Its id starts with `ps_`. Creating a session with an `amount` creates the payment for you. That is the normal path. You can also create a payment first and attach a session to it with `payment`, for example to keep one payment id across a retried checkout. ## Lifecycle ``` POST /v1/payment_sessions → Payment: created, Session: open payer completes checkout → Payment: succeeded | failed, Session: complete 24 hours pass unpaid → Session: expired POST /v1/refunds → Payment: refunded_amount grows, then status refunded ``` ### Payment status | Status | Meaning | | --- | --- | | `created` | Awaiting the payer. Also the state of a bank invoice until the bank confirms payment. | | `processing` | Authorised, awaiting final confirmation from the processor. Brief for cards. | | `succeeded` | Charged. Fulfil the order. | | `failed` | Declined or authentication failed. `failure_reason` says why. The payer can try again in the same session. | | `canceled` | Cancelled before completion. | | `refunded` | Fully refunded. Partial refunds keep `succeeded` and increase `refunded_amount`. | ### Session status | Status | Meaning | | --- | --- | | `open` | The payer can still pay. Sessions live for 24 hours. | | `complete` | A payment attempt finished, or a bank invoice was issued. | | `expired` | Nobody paid within 24 hours. Create a new session. | ## Amounts and currencies Amounts are integers in the currency's smallest unit, paired with an ISO 4217 `currency`. Icelandic króna has no minor unit in circulation, so `1990` is 1.990 kr. Euro and the other currencies use cents: `1990` is €19.90. Supported currencies: _(Currencies table: see the HTML page)_ Amounts must be at least 1 and at most 2,000,000,000. ## Hosted or embedded? | | Hosted | Embedded | | --- | --- | --- | | Where the payer pays | `checkout.borga.is` | A modal on your page, loaded by borga.js | | Integration | One server call and a redirect | Server call plus a few lines of browser code | | Payment methods | Cards, wallets, bank invoice | Cards and wallets | | Needs a publishable key | No | Yes | | Best for | Most shops, invoices, links you send by email | Checkouts that must not leave the page | Both use the same session object and the same webhooks. Start with [hosted checkout](/payments/hosted-checkout); switch to [embedded](/payments/embedded-checkout) if you need it. ## Fulfilment: webhooks, not redirects The redirect back to your `return_url` tells you the payer finished the checkout UI, nothing more. A bank invoice is paid days later, a slow 3-D Secure flow may still be settling, and a payer can close the tab before the redirect. Fulfil from the `payment.succeeded` webhook and use the return page only to show status. See [Webhooks](/webhooks). ## Saving cards and subscriptions A session can ask Borga to save the card for later (`save_payment_method: true`) or start a subscription when it succeeds (`subscription`). Both attach the card to a Customer as a PaymentMethod. See [Saved cards](/payments/saved-cards) and [Subscriptions](/billing/subscriptions). ## Invoices When a payment succeeds and you have an accounting provider connected, Borga books a paid sales invoice there and links it to the payment as an Invoice object. Refunds produce credit notes. See [Invoicing](/billing/invoicing). --- # Hosted checkout Source: https://docs.borga.is/payments/hosted-checkout > Redirect the payer to a Borga-hosted page that handles cards, wallets, 3-D Secure and bank invoices. Hosted checkout is the simplest integration and supports every payment method. Your server creates a payment session, you redirect the payer to its `url` on `checkout.borga.is`, Borga collects the payment, and the payer is sent back to your `return_url`. Card details never touch your servers, which keeps you in the lightest PCI scope (SAQ A). **Create the session** Hosted is the default mode. `return_url` and `cancel_url` are required. ```ts Node.js const session = await borga.paymentSessions.create( { amount: 12900, currency: "ISK", customer_email: "anna@example.is", external_reference: "order_5678", return_url: "https://yoursite.is/order/complete", cancel_url: "https://yoursite.is/cart", enabled_methods: ["card", "apple_pay", "google_pay"], locale: "is", metadata: { order_id: "5678" }, }, // Your own idempotency key makes retries across processes safe too. { idempotencyKey: "order_5678_checkout" }, ); // Send the payer to the hosted page. redirect(session.url!); ``` ```bash curl curl https://api.borga.is/v1/payment_sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_5678_checkout" \ -d '{ "amount": 12900, "currency": "ISK", "customer_email": "anna@example.is", "external_reference": "order_5678", "return_url": "https://yoursite.is/order/complete", "cancel_url": "https://yoursite.is/cart", "enabled_methods": ["card", "apple_pay", "google_pay"], "locale": "is", "metadata": { "order_id": "5678" } }' ``` Sessions stay `open` for 24 hours. Send an `Idempotency-Key` (the SDK does automatically) so a retried request cannot create two sessions; see [Idempotency](/api/idempotency). **Redirect the payer** Send the browser to `session.url` with an HTTP 303 redirect from your server, or set `window.location.href` in the browser. The payer sees your merchant name and logo, the amount, wallet buttons when available, the card form, and a bank-invoice tab if enabled. **Handle the return** On success Borga shows a confirmation for about two seconds and then redirects to `return_url` with `session=ps_…` appended as a query parameter. Read the session and branch on `payment_status`: ```ts Node.js // GET /order/complete?session=ps_… (Next.js App Router route handler) export async function GET(request: Request) { const sessionId = new URL(request.url).searchParams.get("session"); if (!sessionId) return new Response("Missing session", { status: 400 }); const session = await borga.paymentSessions.retrieve(sessionId); switch (session.payment_status) { case "succeeded": return Response.redirect(`/thank-you?order=${session.id}`, 303); case "created": case "processing": // Bank invoices and slow 3-D Secure flows land here. Show a // "we're confirming your payment" page and finish on the webhook. return Response.redirect("/order/pending", 303); default: return Response.redirect("/order/failed", 303); } } ``` Treat the return page as a status page. The authoritative signal is the [`payment.succeeded` webhook](/webhooks), which also covers payers who never make it back to your site. **Handle cancellation and failure** If the payer uses the back link, Borga sends them to `cancel_url` unchanged. If a card is declined, the checkout shows the error and lets the payer try again; the session stays open. If the session expires, the page offers a link to `cancel_url`. In all of these cases no payment succeeded, and there is nothing to clean up on Borga's side. ## Session options The most useful fields. The full list is in the [API reference](/api/payment-sessions). | Field | Purpose | | --- | --- | | `amount`, `currency` | What to charge. `currency` defaults to `ISK`. | | `return_url`, `cancel_url` | Where the payer lands afterwards. HTTPS on an allowed domain in live mode; `http://localhost` is fine in test mode. | | `external_reference` | Your order or invoice number. Copied to the Payment and to webhook payloads so you can match them. | | `metadata` | Any JSON object. Copied to the Payment. | | `customer_email` | Pre-fills the email field and locks it. | | `customer` | Attach the payment to an existing Customer, needed for saved cards and subscriptions. | | `enabled_methods` | Restrict or extend the methods shown. Default `["card", "apple_pay", "google_pay"]`; add `"bank_invoice"` to offer krafa. | | `locale` | `is` (default) or `en`. Other language tags fall back to English. | | `save_payment_method` | Save the card for later charges. See [Saved cards](/payments/saved-cards). | | `subscription` | Start a subscription with these prices on success. See [Subscriptions](/billing/subscriptions). | ## Allowed redirect domains Borga only redirects to domains you have registered, so a compromised integration cannot send payers to a phishing page. Add them under **Settings → Account → Allowed redirect domains**. Matching is on the hostname exactly, so list `shop.example.is` and `www.shop.example.is` separately if you use both. Attempts to use other domains fail at session creation with [`redirect_url_not_allowed`](/errors/redirect_url_not_allowed). ## Branding The merchant name, logo and primary colour shown on the checkout come from **Settings → Account**. The page is served in Icelandic by default and in English when `locale` is `en`. ## Payment links without code Every session `url` is a shareable payment link valid for 24 hours. Creating sessions from a small internal tool, or straight from an invoicing workflow, is a common way to take payments by email or chat before building a full checkout. --- # Embedded checkout Source: https://docs.borga.is/payments/embedded-checkout > Open Borga checkout as a modal on your own page with borga.js, using a publishable key and a per-session client secret. Embedded checkout keeps the payer on your page. Your server creates a session in `embedded` mode and hands two values to the browser: the session id and a one-time `client_secret`. The borga.js script opens a modal that loads the checkout, and calls back into your page when the payment completes. Under the hood the modal loads `checkout.borga.is` in an iframe, and the card fields live in the card processor's iframe inside it. Your page never sees card data, and neither does Borga: the PCI boundary is the same as hosted checkout. > **Note** > Embedded checkout supports cards, Apple Pay and Google Pay. Bank invoices (krafa) need the hosted page. **Create a publishable key** Under **Developers**, create a **publishable** key and add the origins your checkout pages run on, for example `https://shop.example.is`. Origins are scheme and host only, no path. `localhost` and `127.0.0.1` are always allowed for development. **Create an embedded session on your server** Use your secret key. Set `mode` to `embedded` and `origin` to the page that will open the modal. The response includes a `client_secret` (`pcs_…`) that is only ever returned once. ```ts Node.js // POST /api/checkout (your server) export async function POST(request: Request) { const { cartId } = (await request.json()) as { cartId: string }; const amount = await totalForCart(cartId); const session = await borga.paymentSessions.create({ amount, currency: "ISK", mode: "embedded", origin: "https://yoursite.is", // the page that will open the modal external_reference: cartId, }); // Only these two values go to the browser. Never the secret key. return Response.json({ sessionId: session.id, clientSecret: session.client_secret, }); } declare function totalForCart(cartId: string): Promise; ``` Return only `id` and `client_secret` to the browser. The amount is locked on the server; nothing the browser sends can change it. **Open the modal in the browser** Load borga.js from `https://js.borga.is/v1/borga.js`, construct a client with your publishable key, and call `checkout.open` with the two values from your server. ```html HTML + JS ``` ```tsx React "use client"; import { useBorgaCheckout } from "@borga/react"; export function PayButton({ cartId }: { cartId: string }) { const checkout = useBorgaCheckout({ publishableKey: process.env.NEXT_PUBLIC_BORGA_PUBLISHABLE_KEY!, onError: (err) => console.error(err.message), }); async function pay() { const { sessionId, clientSecret } = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ cartId }), }).then((r) => r.json() as Promise<{ sessionId: string; clientSecret: string }>); checkout.open({ sessionId, clientSecret, onComplete: ({ paymentId }) => { window.location.href = `/thank-you?payment=${paymentId}`; }, }); } return ; } ``` `open()` returns a handle with `close()` if you need to dismiss the modal yourself. Pressing Escape or clicking the backdrop closes it and fires `onCancel`. **Fulfil from the webhook** `onComplete` fires when the checkout reports success and is the right moment to move the payer to a confirmation page. Ship the order from the [`payment.succeeded` webhook](/webhooks), which fires whether or not the browser is still open. ## Callbacks | Callback | When | Payload | | --- | --- | --- | | `onComplete` | The payment succeeded | `{ paymentId, status: "succeeded" }` | | `onCancel` | The payer closed the modal without paying | none | | `onError` | The session could not load, or the payment failed in a way the payer cannot retry | `{ message, code? }` | ## Creating sessions from the browser A publishable key can also create embedded sessions directly from the browser by calling `POST /v1/payment_sessions` with `Authorization: Bearer pk_test_…`. The browser's `Origin` header must be in the key's allowed origins, and only `mode: "embedded"` is accepted. Since the amount then comes from the browser, use this only when the server-side amount is verified later, for example by comparing the webhook payload against your order before fulfilling. ## 3-D Secure Most challenges complete inside the modal. When an issuer insists on a full-page redirect, the checkout returns to `checkout.borga.is/…/3ds-return` and the modal continues automatically. You do not need to handle this. ## Content Security Policy If your site sets a CSP, allow these origins on the page that opens checkout: ``` script-src 'self' https://js.borga.is; frame-src https://checkout.borga.is; connect-src 'self' https://api.borga.is; ``` Add `https://*.straumur.is` to `frame-src` and `connect-src` if you see the card form blocked; the card processor loads its own iframe inside the checkout page. ## React `@borga/react` wraps the script loader in a hook, `useBorgaCheckout`, that returns `open` and `close`. It loads borga.js once per page and is marked `"use client"`. The package is built from the Borga monorepo alongside `@borga/node`; until it is on npm, install it from the repository or copy the hook, which is about 80 lines with no dependencies beyond React. ## Framework-agnostic borga.js is a plain script that sets `window.Borga`. Vue, Svelte, Angular and vanilla pages use it the same way as the HTML example above. Load it only in the browser: importing it during server rendering throws, because it touches `document`. --- # Bank invoices (krafa) Source: https://docs.borga.is/payments/bank-invoices > Let payers settle in their online bank. The claim is issued through your accounting provider and confirmed by webhook when paid. A bank invoice is an Icelandic krafa: a claim that appears in the payer's online banking (netbanki) and is paid from there. There is no card and no email. Payers who prefer it are often companies and public bodies, and it is the natural fit for invoice-style purchases. Borga does not talk to the banks itself. The claim is created by your accounting provider, which is why a connected **PayDay** account is required. DK+ does not expose bank claims through its API, so merchants on DK+ cannot offer this method yet. ## Requirements 1. PayDay connected in the current mode under **Settings → Accounting system**. 2. Bank invoices switched on under **Settings → Bank invoice**. The same page sets the due date (default 14 days) and whether the bank-invoice tab comes first or second on the checkout. 3. A hosted session with `"bank_invoice"` in `enabled_methods`. Sessions that ask for `bank_invoice` without these fail with [`bank_invoice_not_enabled`](/errors/bank_invoice_not_enabled), [`accounting_link_required`](/errors/accounting_link_required) or [`provider_no_bank_invoice`](/errors/provider_no_bank_invoice). ## Create the session ```ts Node.js const session = await borga.paymentSessions.create({ amount: 49900, currency: "ISK", enabled_methods: ["card", "bank_invoice"], // Optional. When supplied, the fields are shown locked in checkout. customer_email: "anna@example.is", customer_kennitala: "0101302989", return_url: "https://yoursite.is/order/complete", cancel_url: "https://yoursite.is/cart", external_reference: "invoice_2026_0142", }); ``` ```bash curl curl https://api.borga.is/v1/payment_sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "amount": 49900, "currency": "ISK", "enabled_methods": ["card", "bank_invoice"], "customer_email": "anna@example.is", "customer_kennitala": "0101302989", "return_url": "https://yoursite.is/order/complete", "cancel_url": "https://yoursite.is/cart", "external_reference": "invoice_2026_0142" }' ``` `customer_email` and `customer_kennitala` are optional. When you supply them, the checkout shows them as locked fields so the claim is issued to the right person; when you leave them out, the payer types them in. The kennitala is checksum-validated on both sides, and an invalid one fails with [`invalid_kennitala`](/errors/invalid_kennitala). ## What the payer sees On the checkout page the payer picks **Bank invoice**, confirms their kennitala and email, and submits. Borga creates the claim through PayDay and shows a confirmation with the payment reference and the account to pay into. The claim then appears in their netbanki, and they are redirected to `return_url` like any other payer. ## What you see The session becomes `complete` immediately, but the payment stays `created` until the bank confirms payment. Borga polls PayDay every 15 minutes and, when the claim shows as paid, marks the payment `succeeded` and sends `payment.succeeded`. Expect this to take hours to days depending on when the payer pays. Because of this delay: - Your return page must handle `payment_status: "created"` gracefully. Say that the invoice has been sent and the order will be confirmed when paid. - Fulfil only from the `payment.succeeded` webhook. - Watch `invoice.voided` for claims that were cancelled in PayDay before payment. The Invoice object linked to the payment carries `sent_to_bank`, the payment reference and the creditor account, and the `invoice.created` event for a bank invoice includes them too. ## Subscriptions by bank invoice Subscriptions can collect by bank invoice instead of card: create them with `collection_method: "send_invoice"` and a `days_until_due`. Each period Borga issues a claim through PayDay and marks the invoice paid when the bank confirms. See [Subscriptions](/billing/subscriptions). ## Fees 1.0% of the amount plus the bank's own claim fees, passed through at cost. No fee is charged if the claim is never paid. --- # Saved cards Source: https://docs.borga.is/payments/saved-cards > Tokenise a card during checkout, attach it to a customer, and let subscriptions charge it later. Borga can save a card at the end of a successful checkout. The card is tokenised by the processor, Borga stores only the token plus brand, last four digits and expiry, and the result is a **PaymentMethod** (`pm_…`) attached to a **Customer** (`cus_…`). Saved cards are used by [subscriptions](/billing/subscriptions), which charge the customer's default payment method every period without the payer being present. > **Note** > One-off charges against a saved card, outside a subscription, are not available through the API yet. Today the way to charge a returning customer is a new checkout session, or a subscription. ## Save a card during checkout Create a customer, then a session with `customer` and `save_payment_method: true`. The session can charge a real amount for a purchase, or a small amount if you only want to collect the card. ```ts Node.js const customer = await borga.customers.create({ email: "anna@example.is", name: "Anna Jónsdóttir", }); const session = await borga.paymentSessions.create({ amount: 2990, currency: "ISK", customer: customer.id, save_payment_method: true, // tokenise the card after this payment return_url: "https://yoursite.is/account/cards?saved=1", cancel_url: "https://yoursite.is/account/cards", }); ``` When the payment succeeds, Borga tokenises the card and sends `payment_method.attached` with the PaymentMethod as `data`. Store `data.id` if you need to refer to the card later. If you did not pass `customer`, Borga creates one from the email the payer typed at checkout. Sessions with `subscription` save the card automatically; you do not need to set `save_payment_method`. ## Manage saved cards ```ts Node.js const { data: cards } = await borga.paymentMethods.list({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", }); for (const pm of cards) { console.log(pm.id, pm.card?.brand, pm.card?.last4, pm.status); } // Choose which card subscriptions charge. await borga.paymentMethods.setDefault(cards[0].id); // Remove a card the customer no longer wants on file. await borga.paymentMethods.detach(cards[1].id); ``` - `list` returns the customer's active cards. - `set_default` chooses which card subscriptions charge. The first saved card becomes the default automatically. - `detach` removes a card. Detached cards cannot be charged again and stay visible with `status: "detached"` for your records. A PaymentMethod's `card` object has `brand`, `last4`, `exp_month`, `exp_year` and `wallet_type` (`apple_pay` or `google_pay` when the card came from a wallet). Expired cards move to `status: "expired"`. ## Security Borga never sees the card number. The processor returns an opaque recurring token that is valid only for your merchant, and Borga stores it encrypted. Nothing in the API returns the token. --- # Refunds Source: https://docs.borga.is/payments/refunds > Refund all or part of a payment, and let Borga issue the matching credit note in your accounting system. A refund returns money to the payer's card. Refunds are their own object (`ref_…`) so a payment can have several partial refunds over time. Bank-invoice payments cannot be refunded through Borga; issue a credit note in your accounting system and pay the customer back directly. ## Create a refund ```ts Node.js // Partial refund. Omit `amount` to refund everything that is left. const refund = await borga.refunds.create( { payment: "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", amount: 990, reason: "Returned one item", metadata: { ticket: "support_1729" }, }, { idempotencyKey: "refund_support_1729" }, ); console.log(refund.status); // "pending" until payment.refunded arrives ``` ```bash curl curl https://api.borga.is/v1/refunds \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: refund_support_1729" \ -d '{ "payment": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 990, "reason": "Returned one item" }' ``` Omit `amount` to refund the full remaining balance. The refundable balance is the payment's `amount` minus `refunded_amount` minus any refunds still pending, so two concurrent partial refunds cannot exceed the payment. Only payments in status `succeeded` or `processing` can be refunded. Always send an `Idempotency-Key` with refunds. A retried request with the same key returns the original refund instead of creating a second one; see [Idempotency](/api/idempotency). ## Statuses | Status | Meaning | | --- | --- | | `pending` | Accepted by the processor, awaiting confirmation. `refund.created` fired. | | `succeeded` | Confirmed. The payment's `refunded_amount` has increased and `payment.refunded` fired. When fully refunded, the payment's status becomes `refunded`. | | `failed` | The processor rejected the refund. Nothing was returned. | Confirmation usually arrives within seconds. The money reaches the payer's card in a few business days depending on their bank. ## Credit notes If the original payment was booked as an invoice in PayDay or DK+, a succeeded refund creates a **credit note** there for the refunded amount and Borga records it as a CreditNote object (`cn_…`) with `credit_note.created`. Your books therefore show the sale and the reversal without manual work. See [Invoicing](/billing/invoicing). ## Errors | Code | Meaning | | --- | --- | | [`payment_not_refundable`](/errors/payment_not_refundable) | The payment is not `succeeded` or `processing`. | | [`amount_too_large`](/errors/amount_too_large) | More than the refundable balance. | | [`card_payment_reverse_failed`](/errors/card_payment_reverse_failed) | The processor refused. The refund is recorded as `failed`. | | [`card_payment_reverse_uncertain`](/errors/card_payment_reverse_uncertain) | The processor did not answer. The refund stays `pending`; check its status before retrying. | ## Fees The fee on the original payment is not returned. There is no fee on the refund itself. --- # Subscriptions Source: https://docs.borga.is/billing/subscriptions > Recurring billing with fixed and seat-based prices, trials, proration, discounts and bank-invoice collection. A **Subscription** (`sub_…`) bills a Customer for one or more Prices on a schedule. Borga owns the cycle: at the end of each period it builds an invoice for what was used, books it in your accounting system, and collects it from the customer's saved card or by bank invoice. Failed charges are retried automatically. ## Building blocks | Object | Purpose | | --- | --- | | Product (`prod_`) | What you sell, for example "Pro plan". | | Price (`price_`) | How much and how often: `unit_amount`, `currency`, `recurring.interval`. A product can have several prices. | | Customer (`cus_`) | Who pays. Needs a saved card for `charge_automatically`, or a kennitala for `send_invoice`. | | Subscription (`sub_`) | The customer's commitment to a set of prices. | | Subscription item (`si_`) | One price within the subscription, with a `quantity` (seats). | Create the catalogue once: ```ts Node.js const product = await borga.products.create({ name: "Pro plan", description: "Everything in Starter plus priority support", }); const monthly = await borga.prices.create({ product: product.id, type: "recurring", currency: "ISK", unit_amount: 4990, // 4.990 kr. per seat per month recurring: { interval: "month", interval_count: 1 }, }); const yearly = await borga.prices.create({ product: product.id, type: "recurring", currency: "ISK", unit_amount: 49900, recurring: { interval: "year" }, }); ``` Every item in a subscription must share the same billing interval and currency. Quarterly and half-yearly plans use `interval: "month"` with `interval_count` 3 or 6. ## Billing model Borga bills **in arrears**: when a period ends, it issues an invoice for that period (licensed items times quantity, metered usage, prorations and one-off items) and collects it. The invoice appears as `draft`, is finalised in your accounting provider, then paid. A trial period is free. The subscription is `trialing` until the trial ends, becomes `active`, and the first paid period is billed when it ends. ## Start a subscription ### From a checkout session The simplest path when the customer does not have a saved card yet. Pass `subscription` on a hosted session: the checkout collects and saves the card, charges the session `amount` immediately, and creates the subscription when the payment succeeds. ```ts Node.js // One hosted session collects the card, charges the first period and // starts the subscription. The card is saved automatically. const session = await borga.paymentSessions.create({ amount: 4990, currency: "ISK", customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", subscription: { items: [{ price: "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op", quantity: 3 }], }, return_url: "https://yoursite.is/billing/welcome", cancel_url: "https://yoursite.is/pricing", }); // Redirect to session.url. Listen for subscription.created. ``` The session `amount` is a separate, immediate charge recorded on the subscription's metadata as `initial_payment_id`. Because periods are billed when they end, use it for a setup fee or a small verification amount rather than the first period. You receive `payment.succeeded`, `payment_method.attached`, `subscription.created` and `subscription.active`. ### From the API When the customer already has a saved card (see [Saved cards](/payments/saved-cards)), create the subscription directly. The customer's default payment method is charged each period. ```ts Node.js // The customer already has a saved card (payment_method.attached fired). const subscription = await borga.subscriptions.create({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", items: [{ price: "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op", quantity: 3 }], collection_method: "charge_automatically", trial_period_days: 14, metadata: { plan: "pro" }, }); console.log(subscription.status); // "trialing" console.log(subscription.current_period_end); ``` ```bash curl curl https://api.borga.is/v1/subscriptions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "items": [{ "price": "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op", "quantity": 3 }], "collection_method": "charge_automatically", "trial_period_days": 14 }' ``` ### Collect by bank invoice Set `collection_method` to `send_invoice` and a `days_until_due`. Each period Borga issues a krafa through PayDay to the customer's kennitala instead of charging a card. Requires bank invoices to be enabled; see [Bank invoices](/payments/bank-invoices). ```ts Node.js // Bill by bank invoice: each period Borga issues a krafa through the // merchant's accounting provider and the customer pays it in their bank. const subscription = await borga.subscriptions.create({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", // needs a kennitala items: [{ price: "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op" }], collection_method: "send_invoice", days_until_due: 14, }); ``` ## Change a subscription ```ts Node.js const id = "sub_9Qw1Er3Ty5Ui7Op9As1Df3Gh"; // Change seats; Borga prorates the difference on the next invoice. const [item] = (await borga.subscriptions.retrieve(id)).items; await borga.subscriptionItems.update(item.id, { quantity: 5, proration_behavior: "create_prorations", }); // Add a one-off charge to the next invoice. await borga.invoiceItems.create({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", subscription: id, amount: 15000, description: "Onboarding workshop", }); // Cancel when the period ends (reversible with uncancel). await borga.subscriptions.cancel(id, { at_period_end: true }); await borga.subscriptions.uncancel(id); // Pause and resume billing. await borga.subscriptions.pause(id); await borga.subscriptions.resume(id); ``` - **Seats.** Update the item's `quantity`. With `proration_behavior: "create_prorations"` (default) the difference for the rest of the period is added to the next invoice; `always_invoice` bills it right away; `none` skips proration. - **Add or remove prices.** Create or delete subscription items with the same `proration_behavior` options. - **One-off charges.** Create an invoice item on the subscription; it lands on the next invoice. Negative amounts are credits. - **Discounts.** `discount` takes `amount_off` or `percent_off`. Today the only `duration` is `once`, applied to the next invoice. - **Cancel.** `cancel` with `at_period_end: true` sets `cancel_at_period_end` and bills a final invoice for the current period; `uncancel` reverses it. Without the flag the subscription ends immediately. - **Pause and resume.** `pause` stops billing; `resume` picks up with the same anchor. Every change emits `subscription.updated`; see the [event catalog](/api/events). ## Statuses | Status | Meaning | | --- | --- | | `trialing` | In a free trial. Becomes `active` when it ends. | | `active` | Billing normally. | | `past_due` | A charge failed and retries were exhausted, or a bank invoice went overdue. Billing continues; a later successful payment returns it to `active`. | | `unpaid` | Reserved for future dunning policy; not set today. | | `paused` | Paused by you. No invoices are created. | | `canceled` | Ended. Final invoice issued if the cancellation was at period end. | ## Failed payments When a card charge for an invoice fails, Borga emits `invoice.payment_failed` and retries after 1, 3, 5 and 7 days (cumulative days 1, 4, 9 and 16 after the first failure). Each attempt emits another `invoice.payment_failed` with the attempt number, and the last one carries `final: true`. When the schedule is exhausted the subscription moves to `past_due` and `subscription.past_due` fires. Use these events to prompt the customer to update their card; a new saved card set as default is used by the next attempt. For `send_invoice` subscriptions, an invoice that passes its due date emits `invoice.overdue` and also moves the subscription to `past_due`. ## Invoices for subscriptions Each cycle emits `invoice.created`, then `invoice.finalized` once the accounting provider has accepted it, then `invoice.paid` or `invoice.payment_failed`. Invoice line items carry the price's `accounting_code` if set, so revenue lands on the right account. See [Invoicing](/billing/invoicing) and the [Invoices API](/api/invoices). --- # Usage-based billing Source: https://docs.borga.is/billing/usage-based > Meter events from your system, include free units, and price usage by tier on each subscription invoice. Usage-based (metered) billing charges customers for what they consume. You define a **Meter** that turns raw events into a quantity, attach a metered **Price** to it, and send **usage events** as things happen. At the end of each billing period Borga aggregates the events, subtracts included units, prices the rest against the tiers and adds a line to the subscription invoice. **Define a meter** A meter names the event it counts and how to aggregate it. ```ts Node.js // Count API requests const requests = await borga.meters.create({ name: "API requests", event_name: "api_request", aggregate_type: "count", unit_label: "requests", }); // Sum tokens, priced per million const tokens = await borga.meters.create({ name: "LLM tokens", event_name: "llm_completion", aggregate_type: "sum", aggregate_property: "metadata.total_tokens", unit_label: "tokens", unit_multiplier: 1_000_000, }); ``` | `aggregate_type` | Result for the period | | --- | --- | | `count` | Number of matching events | | `sum` | Sum of `aggregate_property` across events | | `max`, `min`, `avg` | Highest, lowest or mean value of `aggregate_property` | | `unique` | Number of distinct values of `aggregate_property`, for example active users | `aggregate_property` is a path into the event's metadata such as `metadata.total_tokens`. `filter` restricts which events count, for example `{ "metadata.tier": "pro" }`. `unit_multiplier` divides the aggregate before pricing so you can store raw tokens and price per million. **Create a metered price** A metered price references the meter and prices its quantity, optionally with free `included_units` and graduated or volume tiers. ```ts Node.js const price = await borga.prices.create({ product: "prod_4Gh6Ij8Kl0Mn2Op4Qr6St8Uv", type: "recurring", currency: "ISK", recurring: { interval: "month", usage_type: "metered" }, meter: "mtr_1Zx3Cv5Bn7Mq9We1Rt3Yu5Io", included_units: 10_000, // first 10k requests are free tiers_mode: "graduated", tiers: [ { up_to: 100_000, unit_amount: 2 }, // 2 kr. per request { up_to: null, unit_amount: 1 }, // 1 kr. above 100k ], }); ``` `graduated` tiers price each band separately (the first 100k at one rate, the rest at another). `volume` tiers price the whole quantity at the rate of the band it lands in. Combine a metered price with a fixed price in one subscription for base-fee-plus-overage plans. **Send usage events** Report events as they happen, singly or in batches of up to 1000. Identify the customer by Borga id or by your own `external_customer_id`, which matches `Customer.external_id`. ```ts Node.js // Single event await borga.usageEvents.create({ event_name: "api_request", customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", idempotency_key: "req_01J9X2K7M3", // dedupes retries metadata: { endpoint: "/v1/chat" }, }); // Batches of up to 1000, identified by your own customer ids await borga.usageEvents.createBatch([ { event_name: "llm_completion", external_customer_id: "user_12345", timestamp: "2026-09-07T12:00:00Z", idempotency_key: "cmpl_a1", metadata: { total_tokens: 640 }, }, { event_name: "llm_completion", external_customer_id: "user_12345", timestamp: "2026-09-07T12:00:02Z", idempotency_key: "cmpl_a2", metadata: { total_tokens: 1024 }, }, ]); ``` ```bash curl curl https://api.borga.is/v1/events \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "event_name": "api_request", "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "idempotency_key": "req_01J9X2K7M3", "metadata": { "endpoint": "/v1/chat" } }' ``` Always set `idempotency_key` to something derived from the event, such as your request id. Repeated keys are dropped and counted in the response's `duplicates`, so retrying a batch never double-counts. `timestamp` defaults to now and may not be more than 60 seconds in the future. **Show customers their usage** Read a customer's live balance for the current period to build usage dashboards or warnings. ```ts Node.js const balance = await borga.meters.balance( "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "mtr_1Zx3Cv5Bn7Mq9We1Rt3Yu5Io", ); console.log(balance.consumed_units, "of", balance.included_units); console.log("overage:", balance.overage_units); ``` `GET /v1/customer-state/:customerId` returns every active subscription and meter balance for a customer in one call, and `GET /v1/meters/:id/quantities` returns the aggregate for an arbitrary period. ## Included units and rollover `included_units` on a price (or on a subscription item, which overrides the price) are free each period. Set `credit_rollover: true` on the item to carry unused units into the next period, for data or message bundles. When a customer has used 80% or more of their included units, Borga emits `usage.high_watermark` once per period so you can warn them before overage starts. ## At period end The billing cycle reads the meter for the period, applies included and rolled-over units, prices the remainder and adds a `metered` line to the invoice with the unit label. Usage in the new period starts from zero (plus any rollover). Events that arrive after the period closed count towards the period they are timestamped in only if that period is still open, so send events promptly. ## Reference - [Meters API](/api/meters) - [Usage events API](/api/usage-events) - [Prices API](/api/prices) for tiers and included units --- # Invoicing and accounting sync Source: https://docs.borga.is/billing/invoicing > Every sale is booked as a legal invoice in PayDay.is or DK+, refunds become credit notes, and your books need no second system. Icelandic law requires a compliant sales invoice for every sale. Rather than generating invoices itself, Borga connects to the accounting system you already use and creates the invoice there: the provider issues the legal document and PDF, and Borga keeps a reference to it as an **Invoice** object (`inv_…`). Refunds are mirrored as **credit notes** (`cn_…`). ## Supported providers | Provider | Connection | Bank invoices (krafa) | | --- | --- | --- | | PayDay.is | Sign in with PayDay from the dashboard (OAuth) | Yes | | DK+ | Paste an API token generated in DK+ | No | Connect one provider per mode under **Settings → Accounting system**. Test mode connects to the provider's test environment, so you can see invoices appear without touching real books. ## What gets booked | Event in Borga | In your accounting system | | --- | --- | | Card or wallet payment succeeds | A paid sales invoice for the amount, to the customer's name, email and kennitala when known | | Bank invoice requested | An open invoice sent as a bank claim; marked paid when the bank confirms | | Subscription period ends | A finalised invoice with one line per subscription item, collected by card or bank claim | | Refund succeeds | A credit note against the original invoice | The Invoice object records the provider's id and invoice number, the `pdf_url`, totals, tax and the billing details used. Retrieve it with the [Invoices API](/api/invoices) or list a customer's invoices in the [customer portal](/billing/customer-portal). ## Statuses | Status | Meaning | | --- | --- | | `draft` | Created by a billing cycle, not yet sent to the provider | | `open` | Issued and awaiting payment (bank invoices, `send_invoice` subscriptions) | | `paid` | Paid and booked | | `void` | Cancelled in the provider | | `sync_failed` | The provider rejected or failed the request. The payment itself is unaffected. | When a sync fails Borga emits `invoice.sync_failed` with the provider's error, and the invoice shows a **Retry sync** button on the **Invoices** page of the dashboard. Common causes are an expired provider token and VAT codes that do not exist in the provider. ## Accounts and VAT Under **Settings → Accounting system** you map Borga tax rates to your provider's VAT codes and set a default revenue account. Give a Price an `accounting_code` to book its lines to a specific account; it is copied onto each invoice line. ## Without a provider You can take payments before connecting a provider. Borga still records an Invoice object per payment so the history is complete, but nothing is booked anywhere and there is no PDF. Bank invoices are unavailable until PayDay is connected. ## Related - [Bank invoices](/payments/bank-invoices) - [Refunds](/payments/refunds) and credit notes - [Customer portal](/billing/customer-portal) for payer-facing invoice history - [Credit notes API](/api/credit-notes) --- # Customer portal Source: https://docs.borga.is/billing/customer-portal > Give payers a link where they can see and download their invoices, and pay open ones. The customer portal is a page on `checkout.borga.is` that lists one customer's invoices for your merchant, with PDF downloads from your accounting provider and a **Pay** button on open ones. Payers do not need an account: you mint a short-lived link and send it to them. ## Create a portal link ```ts Node.js const portal = await borga.customerPortals.create({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", expires_in: 3600, // seconds; up to 24 hours reusable: true, }); // Link or email portal.url to the customer. console.log(portal.url); // https://checkout.borga.is/portal/cprt_… ``` | Field | Purpose | | --- | --- | | `customer` | The customer whose invoices to show. | | `expires_in` | Seconds until the link stops working. Default 3600 (one hour), minimum 60, maximum 86400 (24 hours). | | `reusable` | Default `true`. Set `false` for a link that is consumed on first open. | | `metadata` | Anything you want to remember, for example which email it was sent in. | The response's `url` is the link to share, and its `status` is `active`, `expired`, `revoked` or `consumed`. Revoke a link early with `DELETE /v1/customer_portals/:id`. Portal links can only be created with a secret key. Mint them on your server when a customer asks for their invoices, when you send a billing email, or behind a "My invoices" button in your own app. ## What the payer sees Your merchant name and logo, then a list of invoices with number, date, amount and status. Each invoice has a **Download PDF** link served through Borga, so your accounting provider's credentials never reach the browser. Open invoices with a hosted payment session show a **Pay now** button. ## Reference - [Customer portals API](/api/customer-portals) - [Invoices API](/api/invoices) --- # Webhooks Source: https://docs.borga.is/webhooks > Receive signed events when payments succeed, invoices are paid and subscriptions change, and use them to fulfil orders. Webhooks are how Borga tells your system what happened. Whenever an object changes, Borga creates an **Event** (`evt_…`) and POSTs it to every endpoint you have registered for that event type. Fulfil orders from webhooks rather than from browser redirects: a redirect only proves the payer finished the checkout UI, while `payment.succeeded` proves the money moved, including for bank invoices paid days later. ## Register an endpoint Open **Webhooks** in the dashboard and add a public HTTPS URL. Choose the event types to receive, or leave the default `*` for all. The endpoint's **signing secret** is shown once when you create it; store it as an environment variable. Endpoints are per mode, so create one for test and one for live. Localhost and private network addresses are rejected. During development, expose your local server with a tunnel and register the tunnel URL; see [Test mode](/test-mode). ## Event shape ```json payment.succeeded { "id": "evt_5Rt8Uv1Wx3Yz5Ab7Cd9Ef1Gh", "type": "payment.succeeded", "created_at": "2026-09-07T12:04:31.512Z", "data": { "id": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 12900, "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": "4242", "wallet_type": null, "created_at": "2026-09-07T12:03:58.201Z", "updated_at": "2026-09-07T12:04:31.498Z" } } ``` `data` is the object the event is about, in the same shape the corresponding `GET` endpoint returns. Some lifecycle events send a smaller summary instead; the [event catalog](/api/events) lists what each type carries. When in doubt, fetch the object by id, since a webhook describes a moment in time and the object may have changed since. ## Verify the signature Every delivery carries a `Borga-Signature` header of the form `t=,v1=`. The HMAC is computed over the timestamp, a period, and the exact request body, keyed with the endpoint's signing secret. Verify it before trusting the payload, and always verify against the raw body: re-serialised JSON will not match. ```ts Next.js import { verifyWebhookSignature, WebhookVerificationError } from "@borga/node"; import type { Payment } from "@borga/node"; // app/api/borga/webhook/route.ts export async function POST(request: Request) { const rawBody = await request.text(); // verify the raw body, never re-serialised JSON let event; try { event = verifyWebhookSignature({ body: rawBody, signature: request.headers.get("Borga-Signature"), secret: process.env.BORGA_WEBHOOK_SECRET!, }); } catch (err) { if (err instanceof WebhookVerificationError) { return new Response("Invalid signature", { status: 400 }); } throw err; } switch (event.type) { case "payment.succeeded": { const payment = event.data as unknown as Payment; await fulfillOrder(payment.external_reference, payment.id); break; } case "payment.failed": // Optional: notify the customer, release reserved stock. break; } // 2xx tells Borga the event is handled. Anything else is retried. return new Response("ok"); } declare function fulfillOrder(orderRef: string | null, paymentId: string): Promise; ``` ```ts Express import { verifyWebhookSignature } from "@borga/node"; // Register with express.raw so req.body is the untouched Buffer. // app.post("/borga/webhook", express.raw({ type: "*/*" }), handler); export function handler( req: { body: Buffer; header(name: string): string | undefined }, res: { sendStatus(code: number): void }, ) { try { const event = verifyWebhookSignature({ body: req.body, signature: req.header("Borga-Signature"), secret: process.env.BORGA_WEBHOOK_SECRET!, }); if (event.type === "payment.succeeded") { // fulfil the order } res.sendStatus(200); } catch { res.sendStatus(400); } } ``` ```ts Manual (Node) import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(rawBody: string, header: string, secret: string): boolean { // Borga-Signature: t=1725710400,v1=5f1c… const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("="))); const timestamp = Number(parts.t); if (!Number.isFinite(timestamp)) return false; if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false; // 5 min tolerance const expected = createHmac("sha256", secret) .update(`${parts.t}.${rawBody}`) .digest("hex"); return ( expected.length === parts.v1?.length && timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(parts.v1, "hex")) ); } ``` `verifyWebhookSignature` from `@borga/node` checks the HMAC in constant time, rejects timestamps more than five minutes old (configurable with `toleranceSeconds`) and returns the parsed event. It throws `WebhookVerificationError` on any problem. ## Respond and retry Return any 2xx status once you have durably recorded or handled the event. Anything else, including a timeout after 10 seconds or a redirect, counts as a failure and Borga retries with this schedule: | Attempt | Delay after previous | | --- | --- | | 2 | 1 minute | | 3 | 5 minutes | | 4 | 30 minutes | | 5 | 2 hours | | 6 | 6 hours | | 7 | 24 hours | | 8 | 48 hours | That is eight attempts over roughly three days. If the last fifteen deliveries to an endpoint all failed, the endpoint is disabled and shown as such in the dashboard; re-enable it there once fixed. The **Webhooks** page lists every delivery with its response code and body, and lets you retry any of them by hand. Do not return 2xx for events you failed to process just to stop retries: that drops the event. Return a 5xx and fix the handler; the retry schedule gives you time. ## Handle duplicates and order Retries mean you can receive the same event twice, and two events for the same object can arrive out of order. Make handlers idempotent by keying on the event `id` or on the object's id and status, and prefer reading the current state from the API over trusting a sequence of webhooks. ## Which events to listen for | Goal | Events | | --- | --- | | Fulfil orders | `payment.succeeded`, plus `payment.failed` to release stock | | Track refunds | `refund.created`, `payment.refunded`, `credit_note.created` | | Provision subscriptions | `subscription.created`, `subscription.active`, `subscription.canceled`, `subscription.past_due` | | Nudge customers to update cards | `invoice.payment_failed`, `invoice.overdue` | | Monitor accounting sync | `invoice.sync_failed` | | Warn about usage | `usage.high_watermark` | The complete list with payloads is in the [event catalog](/api/events). ## Security notes Deliveries come from Borga's servers with `User-Agent: Borga-Webhooks/1.0`. Do not allow-list by IP; verify the signature instead. Rotating a signing secret under **Webhooks → Rotate secret** takes effect immediately, so deploy the new secret to your handler first and rotate right after. --- # Errors Source: https://docs.borga.is/errors > Every error has a stable code, a type that maps to an HTTP status, and a request id to quote when you contact support. Errors share one envelope. `type` tells you the class of problem and maps to the HTTP status; `code` is a stable identifier to branch on; `message` is for humans and may change; `param` names the offending field when there is one. ```json Error response { "error": { "type": "invalid_request_error", "code": "redirect_url_not_allowed", "message": "Domain \"shop.example\" is not in the merchant's allowed redirect domains.", "param": "return_url", "doc_url": "https://docs.borga.is/errors/redirect_url_not_allowed", "request_id": "req_95ef3682c9474dcd99cc31cab949a5e2" } } ``` Each `code` has a page at `/errors/`, which is what `doc_url` points to. The `request_id` is also sent as the `X-Request-Id` response header on every request; include it when writing to support. ## Error types _(ErrorTypes table: see the HTML page)_ ## Handling errors in code The Node SDK throws `BorgaError` for every non-2xx response and for network failures, with the envelope's fields as properties. It retries `429` and `5xx` responses and network errors up to three times with exponential backoff before throwing. ```ts Node.js const borga = new Borga({ apiKey: process.env.BORGA_SECRET_KEY! }); try { await borga.paymentSessions.create({ amount: 1990, currency: "ISK", return_url: "https://yoursite.is/complete", cancel_url: "https://yoursite.is/cart", }); } catch (err) { if (err instanceof BorgaError) { // err.code is stable and safe to branch on; err.message is for humans. if (err.code === "redirect_url_not_allowed") { console.error(`Add the domain in the dashboard (${err.param})`); } else if (err.status === 429) { // The SDK already retried; back off further. } console.error(`[${err.requestId}] ${err.type}/${err.code}: ${err.message}`); } else { throw err; } } ``` Branch on `code`, never on `message`. Treat unknown codes as failures of their `type`: new codes are added over time. ## Validation errors Request bodies are validated before anything else runs. Unknown fields are stripped silently; wrong types or constraint violations return `validation_failed` with the first failing rule in `message` and the field in `param`. ## All error codes _(ErrorCodes table: see the HTML page)_ --- # SDKs and libraries Source: https://docs.borga.is/sdks > Official Node.js client, the borga.js browser script, and what to do from other languages. ## @borga/node The official server-side client. Current version: latest. Requires Node.js 18 or newer and works in any TypeScript or JavaScript project. ```bash npm install @borga/node ``` ```ts const borga = new Borga({ apiKey: process.env.BORGA_SECRET_KEY! }); ``` What it gives you over raw HTTP: - **Types** for every request and response, generated from the same source the API validates against. - **Idempotency keys** on every POST, so a retried call never creates a duplicate. Pass your own with `{ idempotencyKey }` to make retries across processes safe too. - **Retries** with exponential backoff for `429`, `5xx` and network errors (three by default; configurable with `maxRetries`), honouring `Retry-After`. - **Errors** as `BorgaError` instances carrying `type`, `code`, `status`, `param`, `requestId` and the raw body. - **Webhook verification** with `verifyWebhookSignature`. Resources map one-to-one to the API: `paymentSessions`, `payments`, `refunds`, `paymentMethods`, `customers`, `customerPortals`, `products`, `prices`, `subscriptions`, `subscriptionItems`, `invoices`, `invoiceItems`, `creditNotes`, `meters` and `usageEvents`. Each exposes `create`, `retrieve`, `list` and `update` where the API does, plus the resource's actions such as `subscriptions.cancel` or `paymentMethods.setDefault`. Options: `baseUrl` (for a local API), `timeoutMs` (default 60 seconds), `fetch` (custom implementation) and `appInfo` (appended to the `User-Agent`). Every code sample on this site is compiled against the published package in CI, so if a sample is here, it typechecks against the current SDK. ## borga.js The browser script for [embedded checkout](/payments/embedded-checkout), served from `https://js.borga.is/v1/borga.js`. It sets `window.Borga`: ```js const borga = new Borga("pk_test_…"); const handle = borga.checkout.open({ sessionId, clientSecret, onComplete, onCancel, onError }); handle.close(); ``` It has no dependencies and no build step. Load it only in the browser. ## @borga/react A `useBorgaCheckout` hook that loads borga.js once and returns `open` and `close`, for React 18 and 19. It is built in the Borga monorepo next to the Node SDK and is not on npm yet; see [Embedded checkout](/payments/embedded-checkout#react). ## Other languages There are no official Python, Ruby, PHP, Go or .NET clients. The API is plain REST over HTTPS with JSON bodies, bearer-token authentication and HMAC-SHA256 webhooks, so any HTTP client works. Two things to replicate from the Node SDK: 1. Send an `Idempotency-Key` header on every POST; see [Idempotency](/api/idempotency). 2. Retry `429` and `5xx` with backoff, honouring `Retry-After`. The [API reference](/api) shows curl for every endpoint. If you are generating a client, the OpenAPI document that describes request bodies is published in the Borga repository at `apps/docs/openapi/borga-v1.json`. --- # Security Source: https://docs.borga.is/security > PCI scope, how keys and secrets are handled, and what Borga expects from your integration. ## PCI scope Card details are entered into the card processor's own iframe, served from its PCI DSS Level 1 environment, on both the hosted page and inside the embedded modal. Neither your servers nor Borga's ever see a card number, expiry or CVC. Borga receives a token for repeat charges, plus the brand, last four digits and expiry for display. This places merchants using Borga in **SAQ A**, the lightest self-assessment questionnaire, for both hosted and embedded checkout. ## Credentials - **Secret keys** are stored as bcrypt hashes and shown once. Use one key per environment, keep them in a secret store, and rotate under **Developers** if one may have leaked. Rotation revokes the old key immediately. - **Publishable keys** are safe in browsers. They can only create embedded sessions from origins on their allow-list, for a merchant-controlled amount. - **Client secrets** (`pcs_…`) authorise one checkout session for 24 hours and are returned once. Pass them to the browser over HTTPS and nowhere else. - **Webhook signing secrets** are 48 characters of entropy, encrypted at rest, and shown once. Verify every delivery; see [Webhooks](/webhooks). ## Transport and data All endpoints are HTTPS only with HSTS. API requests and responses are logged with their `request_id` but request bodies are not logged. Accounting-provider tokens are encrypted with AES-256-GCM. Test and live data are partitioned by mode on every query. ## Isolation between surfaces `checkout.borga.is` runs on its own origin with no shared cookies and a strict Content Security Policy, and only talks to a small set of public endpoints that authenticate with the session id (and the client secret for embedded sessions). Merchant APIs are never exposed there. `dashboard.borga.is` authenticates with Kenni electronic ID and is separate from the API-key surface. ## What Borga expects from you - Fulfil from verified webhooks, not from redirect parameters or browser callbacks. - Register only the domains you own as redirect domains and publishable-key origins, and use HTTPS everywhere. - Set a Content Security Policy on pages that load borga.js; see [Embedded checkout](/payments/embedded-checkout#content-security-policy). - Send an `Idempotency-Key` on POSTs that move money so retries cannot double-charge. ## Reporting a vulnerability Email [security@borga.is](mailto:security@borga.is) with details and a way to reach you. Please do not test against live merchants; test mode is free and identical in behaviour. --- # Using Borga with AI coding assistants Source: https://docs.borga.is/ai-assistants > Machine-readable versions of these docs for Claude, Cursor, Copilot and friends, and how to keep them from guessing. Large language models are good at wiring up payment APIs, and bad at knowing which of the fifty payment APIs they have seen your code resembles. Give yours the real documentation. ## Feed it the docs | Resource | Use it for | | --- | --- | | [`/llms.txt`](/llms.txt) | An index of every page with a one-line description. Assistants that follow `llms.txt` conventions discover the rest from here. | | [`/llms-full.txt`](/llms-full.txt) | The entire site as one Markdown file. Paste it into context, or add the URL to your assistant's documentation sources. | | Any page with `.md` appended, for example [`/webhooks.md`](/webhooks.md) | Just that page, as Markdown. | Both files are generated from the same source as these pages on every deploy, so they never lag behind the HTML. ## Suggested prompt > Integrate Borga hosted checkout into this app using the `@borga/node` SDK. Use the documentation at https://docs.borga.is/llms-full.txt as the only source of truth for endpoints and fields; do not assume Stripe behaviour. Fulfil orders from the `payment.succeeded` webhook using `verifyWebhookSignature`, and read the session id from the `session` query parameter on the return URL. ## Things assistants get wrong These are the most common mistakes, in case you want to add them to your project's instructions file: - The return URL receives `?session=ps_…`, not a payment id. - Webhook `data` is the object itself, not `data.object`, and there is no `livemode` field. - Amounts in ISK are whole krónur. `1000` is 1.000 kr., not 10 kr. - API keys identify the merchant. There is no merchant id header on API requests. - Webhook endpoints and API keys are created in the dashboard, not through the API. - `/v1/events` ingests usage events for metered billing; it does not list webhook events. - Embedded checkout opens a modal with `borga.checkout.open()`. There is no `` element. - Bank invoices (krafa) are hosted-checkout only and need PayDay connected. --- # API overview Source: https://docs.borga.is/api > Conventions shared by every endpoint: base URL, authentication, request and response format, ids, metadata and versioning. ``` https://api.borga.is ``` The Borga API is REST over HTTPS with JSON bodies. It follows the conventions most developers know from Stripe: predictable resource URLs, `snake_case` fields, prefixed ids, cursor pagination and idempotent POSTs. Where Borga's domain differs, for example bank invoices and accounting sync, the API says so rather than bending the concept. ## Authentication Send a secret key as a bearer token. The key identifies your merchant and its mode; nothing else is needed. ```http Authorization: Bearer sk_test_… ``` Publishable keys (`pk_…`) may only call `POST /v1/payment_sessions` with `mode: "embedded"`, from an allowed origin. Everything else needs a secret key. See [Authentication](/authentication). ## Requests - Bodies are JSON with `Content-Type: application/json`. - Amounts are integers in the currency's smallest unit. ISK has none, so `1990` is 1.990 kr.; euro uses cents. Supported currencies: _(Currencies table: see the HTML page)_ - Timestamps are ISO 8601 strings in UTC, for example `2026-09-07T12:04:31.512Z`. - Unknown fields are ignored; wrong types return `validation_failed`. - POST requests should carry an `Idempotency-Key`. See [Idempotency](/api/idempotency). ## Responses Objects are returned as flat JSON with related objects referenced by id (`"customer": "cus_…"`). A few objects carry an `object` field naming their type; most do not, so key off the id prefix instead. Lists come back as `{ "data": [...], "has_more": true }`; see [Pagination](/api/pagination). Every response includes an `X-Request-Id` header. Errors share one envelope with `type`, `code`, `message`, optional `param`, `doc_url` and `request_id`; see [Errors](/errors). ## Object ids Ids are random strings with a type prefix, so a `pay_…` is always a payment wherever it shows up. _(IdPrefixes table: see the HTML page)_ ## Metadata Most objects accept a `metadata` object of your own keys and values. Borga stores it and returns it unchanged, never interprets it, and copies it from a payment session to the payment it creates. Use it for order numbers, internal ids and anything you want to see in webhooks. ## Test and live Mode is a property of the key. Test objects and live objects never mix, and live keys only work after [going live](/go-live). Everything on these pages behaves identically in both modes. ## Versioning The API is versioned in the path (`/v1`). Backwards-compatible changes (new fields, new endpoints, new event types, new error codes) ship without notice. Breaking changes would get a new path version with a migration period. Write clients to ignore fields and codes they do not recognise. ## Dashboard-only configuration API keys, webhook endpoints, allowed redirect domains, the accounting connection and bank-invoice settings are managed in the [dashboard](https://dashboard.borga.is), not through API keys. Calling those routes with an API key returns `wrong_auth_method`. ## Endpoints - [Payment sessions](/api/payment-sessions): Create checkouts, hosted or embedded. - [Payments](/api/payments): Read payment status and details. - [Refunds](/api/refunds): Return money, fully or partially. - [Payment methods](/api/payment-methods): Saved cards attached to customers. - [Customers](/api/customers): People and companies you bill. - [Customer portals](/api/customer-portals): Payer-facing invoice history links. - [Products and prices](/api/products): Your catalogue. - [Subscriptions](/api/subscriptions): Recurring billing. - [Invoices](/api/invoices): Booked invoices and credit notes. - [Meters and usage](/api/meters): Usage-based billing. --- # Pagination Source: https://docs.borga.is/api/pagination > List endpoints return a page of objects, newest first, with a cursor for the next page. List endpoints return objects in reverse chronological order and accept two paging parameters: **Query parameters** - `limit` (integer, default 25): Page size, 1 to 100. Values out of range fall back to the default or the maximum. - `starting_after` (string): The id of the last object on the previous page. The response starts with the object after it. The response is an envelope with the page and a flag for whether more exists: ```json Response { "data": [ { "id": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 12900, "currency": "ISK", "status": "succeeded" }, { "id": "pay_2Lm4Np6Qr8St0Uv2Wx4Yz6Ab", "amount": 1990, "currency": "ISK", "status": "succeeded" } ], "has_more": true } ``` To walk a list, pass the last id back as `starting_after` until `has_more` is `false`. ```ts Node.js // Walk every succeeded payment, 100 at a time. let startingAfter: string | undefined; do { const page = await borga.payments.list({ status: "succeeded", limit: 100, starting_after: startingAfter, }); for (const payment of page.data) { console.log(payment.id, payment.amount); } startingAfter = page.has_more ? page.data.at(-1)?.id : undefined; } while (startingAfter); ``` ```bash curl curl "https://api.borga.is/v1/payments?status=succeeded&limit=100" \ -H "Authorization: Bearer sk_test_…" # Next page: pass the id of the last object you received. curl "https://api.borga.is/v1/payments?status=succeeded&limit=100&starting_after=pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6" \ -H "Authorization: Bearer sk_test_…" ``` An unknown or foreign `starting_after` id returns [`resource_missing`](/errors/resource_missing). ## Filters Most lists accept filters as additional query parameters, for example `status` on payments or `customer` on subscriptions. They are listed on each resource page. Filters combine with paging. ## Endpoints without cursors A few lists are bounded by nature and return at most `limit` objects with no `has_more`: refunds, credit notes and customer portals. Filter them by their parent (`payment`, `invoice`, `customer`) instead of paging. There is no `ending_before` parameter and no total count. --- # Idempotency Source: https://docs.borga.is/api/idempotency > Retry any POST safely by sending the same Idempotency-Key. Networks fail at the worst moments. If a request to create a refund times out, you cannot know whether it went through. Idempotency keys solve this: send a unique key with the first attempt and reuse it on retries, and Borga returns the original response instead of performing the operation twice. ```http Idempotency-Key: refund:order_5678:item_2 ``` ```ts Node.js // Derive the key from the operation, not from the attempt. Retrying with the // same key returns the original response instead of creating a second refund. await borga.refunds.create( { payment: "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", amount: 990 }, { idempotencyKey: `refund:order_5678:item_2` }, ); ``` ```bash curl curl https://api.borga.is/v1/refunds \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: refund:order_5678:item_2" \ -d '{ "payment": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 990 }' ``` ## Rules - Keys are any string of 1 to 255 characters from `A-Z a-z 0-9 _ - : .`. - Keys are scoped to your merchant and mode. Two merchants can use the same key without interference. - A stored response is kept for **24 hours**. After that the same key starts a new request. - Replays return the original status code and body, plus the headers `Idempotency-Key: ` and `Idempotent-Replayed: true`. - 4xx responses are stored too, so a retried invalid request returns the same error. 5xx responses are not stored, so a retry after a server error runs again. - Only POST requests are affected. GET, PATCH and DELETE ignore the header. ## Choose good keys Derive the key from the business operation, not from the attempt: `refund::` or a UUID you generate once and persist alongside the intent to act. The Node SDK generates a fresh UUID for every POST when you do not pass one, which protects against its own internal retries but not against your process restarting; pass your own key for anything that moves money. ## Errors | Code | HTTP | Meaning | | --- | --- | --- | | [`idempotency_key_in_use`](/errors/idempotency_key_in_use) | 409 | The first request with this key is still running. Wait and retry with the same key. | | [`idempotency_key_payload_mismatch`](/errors/idempotency_key_payload_mismatch) | 409 | The key was already used with a different body or on a different route. Use a new key. | | [`invalid_field`](/errors/invalid_field) with `param: "Idempotency-Key"` | 400 | The key contains disallowed characters or is too long. | --- # Rate limits Source: https://docs.borga.is/api/rate-limits > Per-key limits, the headers that report them, and how to back off. Requests are limited per API key and mode in fixed one-minute windows. | Caller | Limit | | --- | --- | | API key (`sk_…` or `pk_…`) | 1000 requests per minute per key | | Public checkout endpoints (no key) | 60 requests per minute per IP address | Buckets are per route, so a burst of session creations does not starve your payment lookups. ## Headers Every rate-limited response carries the current state: ```http RateLimit-Limit: 1000 RateLimit-Remaining: 993 RateLimit-Reset: 41 ``` `RateLimit-Reset` is seconds until the window resets. When you exceed the limit the API returns `429` with [`rate_limit_exceeded`](/errors/rate_limit_exceeded) and a `Retry-After` header in seconds. ## Backing off Wait at least `Retry-After` seconds, then retry with the same `Idempotency-Key` so the request cannot be duplicated. The Node SDK does this automatically, up to three times with exponential backoff. If you need sustained throughput above the limit, batch where the API allows it (usage events accept 1000 per request) and contact Borga. ## Webhooks and health checks Webhook deliveries to your endpoint are not rate limited by Borga; your endpoint sets the pace by how fast it responds. Retries follow the schedule in [Webhooks](/webhooks). --- # Payment sessions Source: https://docs.borga.is/api/payment-sessions > A payment session is one checkout for one amount, hosted on checkout.borga.is or embedded on your page. Creating a session creates a Payment and a checkout to collect it. Hosted sessions return a `url` to redirect the payer to; embedded sessions return a `client_secret` for [borga.js](/payments/embedded-checkout). Sessions expire after 24 hours. **The payment session object** - `id` (string): Prefixed `ps_`. - `payment` (string): Id of the Payment this session collects. - `payment_status` (string): Current status of the payment. Present when retrieving a session. - `url` (string | null): Hosted checkout URL. `null` for embedded sessions. - `client_secret` (string | null): Embedded sessions only, and only in the create response. Prefixed `pcs_`. - `status` (enum): `open`, `complete` or `expired`. - `mode` (enum): `hosted` or `embedded`. This is the checkout mode, not test or live. - `enabled_methods` (array): Payment methods offered: `card`, `apple_pay`, `google_pay`, `bank_invoice`. - `return_url` (string): Where the payer is sent after paying, with `?session=ps_…` appended. Empty for embedded sessions. - `cancel_url` (string): Where the payer is sent if they go back. - `locale` (string): Checkout language, `is` or `en`. - `customer_email` (string | null): Pre-filled payer email, if any. - `customer_kennitala` (string | null): Pre-filled payer kennitala, if any. - `expires_at` (timestamp): When the session stops accepting payment. - `created_at` (timestamp): ## Create a payment session `POST /v1/payment_sessions` (publishable) Either `amount` or `payment` is required. Hosted sessions also require `return_url` and `cancel_url`; embedded sessions created with a secret key require `origin`. Publishable keys may only create embedded sessions, and the browser's `Origin` header must be on the key's allow-list. **Body** Amount in the smallest currency unit, 1 to 2,000,000,000. Creates a new Payment. Required unless `payment` is set. - `currency` (string, default ISK): ISO 4217 code. Supported: _(Currencies table: see the HTML page)_ - `payment` (string): Attach to an existing Payment in status `created` instead of creating one. - `mode` (enum, default hosted): `hosted` redirects to checkout.borga.is; `embedded` returns a `client_secret` for borga.js. - `return_url` (string): Required for hosted sessions. HTTPS on an allowed redirect domain; `http://localhost` is allowed in test mode. - `cancel_url` (string): Required for hosted sessions. Same rules as `return_url`. - `origin` (string): Required for embedded sessions created with a secret key. The `https://host` of the page that opens the modal. Ignored for publishable keys, which use the request's `Origin`. - `customer` (string): Attach the Payment to a Customer. Required for `save_payment_method` to attach the card somewhere you can find it. - `customer_email` (string): Pre-fills and locks the payer's email. - `customer_kennitala` (string): Pre-fills and locks the payer's kennitala for bank invoices. Checksum-validated. - `external_reference` (string): Your order or invoice reference. Copied to the Payment. - `metadata` (object): Copied to the Payment. - `enabled_methods` (array): Add `bank_invoice` to offer krafa (hosted only, PayDay required). - `locale` (string, default is): Language tag such as `is` or `en`. - `save_payment_method` (boolean): Tokenise the card after payment and attach it to the customer. - `subscription` (object): `{ items: [{ price, quantity? }] }`. Starts a subscription with these recurring prices when the payment succeeds. Implies `save_payment_method`. ```ts Node.js const session = await borga.paymentSessions.create( { amount: 12900, currency: "ISK", customer_email: "anna@example.is", external_reference: "order_5678", return_url: "https://yoursite.is/order/complete", cancel_url: "https://yoursite.is/cart", enabled_methods: ["card", "apple_pay", "google_pay"], locale: "is", metadata: { order_id: "5678" }, }, // Your own idempotency key makes retries across processes safe too. { idempotencyKey: "order_5678_checkout" }, ); // Send the payer to the hosted page. redirect(session.url!); ``` ```bash curl curl https://api.borga.is/v1/payment_sessions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order_5678_checkout" \ -d '{ "amount": 12900, "currency": "ISK", "customer_email": "anna@example.is", "external_reference": "order_5678", "return_url": "https://yoursite.is/order/complete", "cancel_url": "https://yoursite.is/cart", "enabled_methods": ["card", "apple_pay", "google_pay"], "locale": "is", "metadata": { "order_id": "5678" } }' ``` ```json Response { "id": "ps_3kD9mQ2vXb7LpR4tYw8Nz1Ha", "payment": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "url": "https://checkout.borga.is/ps_3kD9mQ2vXb7LpR4tYw8Nz1Ha", "return_url": "https://example.is/order/complete", "cancel_url": "https://example.is/cart", "expires_at": "2026-09-08T12:00:00.000Z", "status": "open", "enabled_methods": ["card", "apple_pay", "google_pay"], "mode": "hosted", "locale": "is", "customer_email": null, "customer_kennitala": null, "created_at": "2026-09-07T12:00:00.000Z", "client_secret": null } ``` Errors: [`missing_amount`](/errors/missing_amount), [`missing_redirect_urls`](/errors/missing_redirect_urls), [`redirect_url_not_allowed`](/errors/redirect_url_not_allowed), [`missing_origin`](/errors/missing_origin), [`origin_not_allowed`](/errors/origin_not_allowed), [`publishable_key_not_allowed`](/errors/publishable_key_not_allowed), [`bank_invoice_not_enabled`](/errors/bank_invoice_not_enabled), [`invalid_kennitala`](/errors/invalid_kennitala), [`resource_not_found`](/errors/resource_not_found). ## Retrieve a payment session `GET /v1/payment_sessions/:id` Returns the session with `payment_status`, which saves a second call on your return page. `client_secret` is never returned here. ```bash curl https://api.borga.is/v1/payment_sessions/ps_3kD9mQ2vXb7LpR4tYw8Nz1Ha \ -H "Authorization: Bearer sk_test_…" ``` There is no list endpoint for sessions and no way to cancel one early; unpaid sessions expire on their own. --- # Payments Source: https://docs.borga.is/api/payments > A payment is one amount owed by one payer, with its outcome. Payments are normally created by a [payment session](/api/payment-sessions). Use this resource to read status and card details, list payments, or create a payment ahead of a session. **The payment object** - `id` (string): Prefixed `pay_`. - `amount` (integer): In the smallest currency unit. - `currency` (string): `created`, `processing`, `succeeded`, `failed`, `canceled` or `refunded`. - `customer` (string | null): Customer the payment belongs to. - `description` (string | null): Your reference, from the session or payment create call. - `metadata` (object): `card`, `wallet` or `bank_invoice` once known. - `refunded_amount` (integer): Total refunded so far. - `failure_reason` (string | null): Processor's reason when `status` is `failed`. - `card_brand` (string | null): For example `visa` or `mc`. - `card_last4` (string | null): `apple_pay` or `google_pay` when a wallet was used. - `created_at` (timestamp): - `updated_at` (timestamp) ## Create a payment `POST /v1/payments` 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. **Body** 1 to 2,000,000,000 in the smallest currency unit. - `currency` (string, default ISK): Must belong to your merchant in this mode. - `description` (string): - `external_reference` (string) - `metadata` (object) ```bash 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" }' ``` ## Retrieve a payment `GET /v1/payments/:id` ```ts Node.js const payment = await borga.payments.retrieve("pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6"); console.log(payment.status); // "succeeded" console.log(payment.card_brand, payment.card_last4); // "visa" "4242" ``` ```bash curl curl https://api.borga.is/v1/payments/pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6 \ -H "Authorization: Bearer sk_test_…" ``` ```json title="Response" { "id": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 12900, "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": "4242", "wallet_type": null, "created_at": "2026-09-07T12:03:58.201Z", "updated_at": "2026-09-07T12:04:31.498Z" } ``` ## List payments `GET /v1/payments` **Query parameters** Filter by payment status. - `limit` (integer, default 25): Up to 100. - `starting_after` (string): Cursor; see [Pagination](/api/pagination). ```bash curl "https://api.borga.is/v1/payments?status=succeeded&limit=50" \ -H "Authorization: Bearer sk_test_…" ``` --- # Refunds Source: https://docs.borga.is/api/refunds > Return all or part of a card payment. Refunds create credit notes in your accounting system. **The refund object** - `id` (string): Prefixed `ref_`. - `payment` (string): The refunded payment. - `amount` (integer): - `currency` (string) `pending`, `succeeded` or `failed`. - `reason` (string | null): Free text you supplied. - `metadata` (object): - `created_at` (timestamp) - `updated_at` (timestamp) ## Create a refund `POST /v1/refunds` The payment must be `succeeded` or `processing`. The refundable balance is the amount minus previous and pending refunds. Send an `Idempotency-Key`. **Body** Payment to refund. - `amount` (integer): Defaults to the full refundable balance. - `reason` (string): Stored on the refund and shown in the dashboard. - `metadata` (object): ```ts Node.js // Partial refund. Omit `amount` to refund everything that is left. const refund = await borga.refunds.create( { payment: "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", amount: 990, reason: "Returned one item", metadata: { ticket: "support_1729" }, }, { idempotencyKey: "refund_support_1729" }, ); console.log(refund.status); // "pending" until payment.refunded arrives ``` ```bash curl curl https://api.borga.is/v1/refunds \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: refund_support_1729" \ -d '{ "payment": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 990, "reason": "Returned one item" }' ``` ```json title="Response" { "id": "ref_6Yz8Ab0Cd2Ef4Gh6Ij8Kl0Mn", "payment": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 990, "currency": "ISK", "status": "pending", "reason": "Returned one item", "metadata": { "ticket": "support_1729" }, "created_at": "2026-09-08T09:12:00.000Z", "updated_at": "2026-09-08T09:12:00.000Z" } ``` Errors: [`payment_not_refundable`](/errors/payment_not_refundable), [`amount_too_small`](/errors/amount_too_small), [`amount_too_large`](/errors/amount_too_large), [`card_payment_reverse_failed`](/errors/card_payment_reverse_failed), [`card_payment_reverse_uncertain`](/errors/card_payment_reverse_uncertain). ## Retrieve a refund `GET /v1/refunds/:id` ```bash curl https://api.borga.is/v1/refunds/ref_6Yz8Ab0Cd2Ef4Gh6Ij8Kl0Mn \ -H "Authorization: Bearer sk_test_…" ``` ## List refunds `GET /v1/refunds` Returns up to `limit` refunds, newest first, without a cursor. Filter by payment. **Query parameters** Only refunds of this payment. - `limit` (integer, default 25): Up to 100. ```bash curl "https://api.borga.is/v1/refunds?payment=pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6" \ -H "Authorization: Bearer sk_test_…" ``` --- # Payment methods Source: https://docs.borga.is/api/payment-methods > Saved cards attached to customers. Created by checkout, charged by subscriptions. Payment methods are created when a session with `save_payment_method` or `subscription` succeeds; there is no create endpoint. See [Saved cards](/payments/saved-cards). **The payment method object** - `id` (string): Prefixed `pm_`. - `object` (string): `payment_method` - `customer` (string): `card` - `status` (enum): `active`, `detached`, `expired` or `failed`. - `card` (object | null): `brand`, `last4`, `exp_month`, `exp_year` and `wallet_type` (`apple_pay`, `google_pay` or `null`). - `created_at` (timestamp): - `detached_at` (timestamp | null) ## Retrieve a payment method `GET /v1/payment_methods/:id` ```bash curl https://api.borga.is/v1/payment_methods/pm_1Ab3Cd5Ef7Gh9Ij1Kl3Mn5Op \ -H "Authorization: Bearer sk_test_…" ``` ## List payment methods `GET /v1/payment_methods` Returns active (not detached) payment methods. **Query parameters** Only this customer's cards. - `limit` (integer, default 25): Up to 100. - `starting_after` (string): Cursor. ```ts Node.js const { data: cards } = await borga.paymentMethods.list({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", }); for (const pm of cards) { console.log(pm.id, pm.card?.brand, pm.card?.last4, pm.status); } // Choose which card subscriptions charge. await borga.paymentMethods.setDefault(cards[0].id); // Remove a card the customer no longer wants on file. await borga.paymentMethods.detach(cards[1].id); ``` ## Set as default `POST /v1/payment_methods/:id/set_default` Makes this the customer's default payment method, used by `charge_automatically` subscriptions. The first card saved to a customer becomes the default automatically. Fails with [`payment_method_inactive`](/errors/payment_method_inactive) if the card is not `active`. ```bash curl -X POST https://api.borga.is/v1/payment_methods/pm_1Ab3Cd5Ef7Gh9Ij1Kl3Mn5Op/set_default \ -H "Authorization: Bearer sk_test_…" ``` ## Detach a payment method `DELETE /v1/payment_methods/:id` Marks the card `detached`. It can no longer be charged, and if it was the default the customer has no default until another card is saved or chosen. Emits `payment_method.detached`. ```bash curl -X DELETE https://api.borga.is/v1/payment_methods/pm_1Ab3Cd5Ef7Gh9Ij1Kl3Mn5Op \ -H "Authorization: Bearer sk_test_…" ``` --- # Customers Source: https://docs.borga.is/api/customers > The people and companies you bill. Customers own saved cards, subscriptions and invoices. **The customer object** - `id` (string): Prefixed `cus_`. - `email` (string): - `name` (string | null) - `phone` (string | null) Icelandic national or company id. Needed for bank-invoice collection. - `external_id` (string | null): Your own id. Usage events can reference it as `external_customer_id`. - `vat_number` (string | null): `line1`, `line2`, `city`, `postal_code`, `country`. - `default_payment_method` (string | null): The `pm_` charged by subscriptions. - `metadata` (object): - `created_at` (timestamp) - `updated_at` (timestamp) ## Create a customer `POST /v1/customers` **Body** - `email` (string) - `name` (string) - `phone` (string) - `kennitala` (string) Your own identifier, unique per customer. - `vat_number` (string): - `billing_address_line1` (string) - `billing_address_line2` (string) - `billing_city` (string) - `billing_postal_code` (string) ISO 3166-1 alpha-2, for example `IS`. - `metadata` (object): ```bash curl https://api.borga.is/v1/customers \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "email": "anna@example.is", "name": "Anna Jónsdóttir", "kennitala": "0101302989", "external_id": "user_12345", "billing_address_line1": "Laugavegur 1", "billing_postal_code": "101", "billing_city": "Reykjavík", "billing_country": "IS" }' ``` ```json title="Response" { "id": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "email": "anna@example.is", "name": "Anna Jónsdóttir", "phone": null, "kennitala": "0101302989", "external_id": "user_12345", "vat_number": null, "billing_address": { "line1": "Laugavegur 1", "line2": null, "city": "Reykjavík", "postal_code": "101", "country": "IS" }, "metadata": {}, "default_payment_method": null, "created_at": "2026-09-07T10:00:00.000Z", "updated_at": "2026-09-07T10:00:00.000Z" } ``` ## Retrieve a customer `GET /v1/customers/:id` ## Update a customer `PATCH /v1/customers/:id` Accepts the same fields as create, all optional. Only the fields you send are changed. **Body** - `email` (string) - `name` (string) - `phone` (string) - `kennitala` (string) Send an empty string to clear. - `vat_number` (string): - `billing_address_line1` (string) - `billing_address_line2` (string) - `billing_city` (string) - `billing_postal_code` (string) - `billing_country` (string) - `metadata` (object) ## List customers `GET /v1/customers` **Query parameters** Exact match, case-insensitive. - `limit` (integer) - `starting_after` (string) --- # Customer portals Source: https://docs.borga.is/api/customer-portals > Short-lived links where a customer can view, download and pay their invoices. See the [customer portal guide](/billing/customer-portal) for what payers see. **The customer portal object** - `id` (string): Prefixed `cprt_`. - `customer` (string): The link to share, on checkout.borga.is. - `status` (enum): `active`, `expired`, `revoked` or `consumed`. - `reusable` (boolean): - `expires_at` (timestamp) - `revoked_at` (timestamp | null) - `last_accessed_at` (timestamp | null) - `metadata` (object) - `created_at` (timestamp) ## Create a portal link `POST /v1/customer_portals` **Body** - `customer` (string) Seconds, 60 to 86400. - `reusable` (boolean, default true): `false` for a single-use link. - `metadata` (object): ```ts Node.js const portal = await borga.customerPortals.create({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", expires_in: 3600, // seconds; up to 24 hours reusable: true, }); // Link or email portal.url to the customer. console.log(portal.url); // https://checkout.borga.is/portal/cprt_… ``` ```json title="Response" { "id": "cprt_2Wx4Yz6Ab8Cd0Ef2Gh4Ij6Kl", "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "url": "https://checkout.borga.is/portal/cprt_2Wx4Yz6Ab8Cd0Ef2Gh4Ij6Kl", "reusable": true, "status": "active", "expires_at": "2026-09-07T13:00:00.000Z", "revoked_at": null, "last_accessed_at": null, "metadata": {}, "created_at": "2026-09-07T12:00:00.000Z" } ``` ## Retrieve a portal link `GET /v1/customer_portals/:id` ## List portal links `GET /v1/customer_portals` Returns up to `limit` links without a cursor. **Query parameters** - `customer` (string) Up to 100. ## Revoke a portal link `DELETE /v1/customer_portals/:id` The link stops working immediately. Revoking an already revoked link is a no-op. --- # Products Source: https://docs.borga.is/api/products > What you sell. A product groups one or more prices. **The product object** - `id` (string): Prefixed `prod_`. - `name` (string): - `description` (string | null) Inactive products cannot get new subscriptions. - `metadata` (object) - `created_at` (timestamp) - `updated_at` (timestamp) ## Create a product `POST /v1/products` **Body** - `name` (string) - `description` (string) - `metadata` (object) ```ts Node.js const product = await borga.products.create({ name: "Pro plan", description: "Everything in Starter plus priority support", }); const monthly = await borga.prices.create({ product: product.id, type: "recurring", currency: "ISK", unit_amount: 4990, // 4.990 kr. per seat per month recurring: { interval: "month", interval_count: 1 }, }); const yearly = await borga.prices.create({ product: product.id, type: "recurring", currency: "ISK", unit_amount: 49900, recurring: { interval: "year" }, }); ``` ## Retrieve a product `GET /v1/products/:id` ## Update a product `PATCH /v1/products/:id` **Body** - `name` (string) - `description` (string) - `active` (boolean) - `metadata` (object) ## List products `GET /v1/products` **Query parameters** - `limit` (integer) - `starting_after` (string) --- # Prices Source: https://docs.borga.is/api/prices > How much a product costs and how often it is billed. Prices are immutable apart from activation and metadata. **The price object** - `id` (string): Prefixed `price_`. - `product` (string): - `active` (boolean) - `currency` (string) `one_time` or `recurring`. - `unit_amount` (integer | null): Per unit, in the smallest currency unit. `null` for tiered prices. - `recurring` (object | null): `interval` (`day`, `week`, `month`, `year`), `interval_count`, `usage_type` (`licensed` or `metered`), `aggregate_usage`. - `tiers` (array | null): Tier objects with `up_to` (number or `null` for the last tier), `unit_amount`, `flat_amount`. - `tiers_mode` (enum | null): `graduated` or `volume`. - `meter` (string | null): Meter for metered prices. - `included_units` (integer | null): Free units per period for metered prices. - `accounting_code` (string | null): Revenue account code copied to invoice lines. - `metadata` (object): - `created_at` (timestamp) - `updated_at` (timestamp) ## Create a price `POST /v1/prices` **Body** - `product` (string) `one_time` or `recurring`. - `currency` (string, default ISK): Per unit. Omit when using tiers. - `recurring` (object): Required for recurring prices: `{ interval, interval_count?, usage_type?, aggregate_usage? }`. `interval` is `day`, `week`, `month` or `year`. - `tiers_mode` (enum): `graduated` prices each band separately; `volume` prices everything at the reached band. - `tiers` (array): `[{ up_to, unit_amount?, flat_amount? }]`, ordered, last tier with `up_to: null`. - `meter` (string): Required when `recurring.usage_type` is `metered`. - `included_units` (integer): Free units per period before tiers apply. - `accounting_code` (string): Copied to invoice lines for this price. - `metadata` (object): ```ts Fixed const product = await borga.products.create({ name: "Pro plan", description: "Everything in Starter plus priority support", }); const monthly = await borga.prices.create({ product: product.id, type: "recurring", currency: "ISK", unit_amount: 4990, // 4.990 kr. per seat per month recurring: { interval: "month", interval_count: 1 }, }); const yearly = await borga.prices.create({ product: product.id, type: "recurring", currency: "ISK", unit_amount: 49900, recurring: { interval: "year" }, }); ``` ```ts Metered const price = await borga.prices.create({ product: "prod_4Gh6Ij8Kl0Mn2Op4Qr6St8Uv", type: "recurring", currency: "ISK", recurring: { interval: "month", usage_type: "metered" }, meter: "mtr_1Zx3Cv5Bn7Mq9We1Rt3Yu5Io", included_units: 10_000, // first 10k requests are free tiers_mode: "graduated", tiers: [ { up_to: 100_000, unit_amount: 2 }, // 2 kr. per request { up_to: null, unit_amount: 1 }, // 1 kr. above 100k ], }); ``` ```json title="Response" { "id": "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op", "product": "prod_4Gh6Ij8Kl0Mn2Op4Qr6St8Uv", "active": true, "currency": "ISK", "type": "recurring", "unit_amount": 4990, "recurring": { "interval": "month", "interval_count": 1, "usage_type": "licensed" }, "tiers": null, "tiers_mode": null, "meter": null, "included_units": null, "accounting_code": null, "metadata": {}, "created_at": "2026-09-07T10:00:00.000Z", "updated_at": "2026-09-07T10:00:00.000Z" } ``` Errors: [`missing_meter`](/errors/missing_meter), [`resource_not_found`](/errors/resource_not_found) for `product` or `meter`. ## Retrieve a price `GET /v1/prices/:id` ## Update a price `PATCH /v1/prices/:id` Amounts and intervals cannot change; create a new price and move subscriptions to it. **Body** Deactivate to stop new subscriptions using it. - `accounting_code` (string): Empty string clears it. - `metadata` (object) ## List prices `GET /v1/prices` **Query parameters** - `product` (string) - `limit` (integer) - `starting_after` (string) --- # Subscriptions Source: https://docs.borga.is/api/subscriptions > A customer's recurring commitment to one or more prices, billed at the end of each period. See the [subscriptions guide](/billing/subscriptions) for the billing model, trials and dunning. **The subscription object** - `id` (string): Prefixed `sub_`. - `object` (string): `subscription` - `customer` (string): `trialing`, `active`, `past_due`, `unpaid`, `canceled` or `paused`. - `collection_method` (enum): `charge_automatically` (saved card) or `send_invoice` (bank invoice). - `days_until_due` (integer | null): For `send_invoice`. - `current_period_start` (timestamp): When the next invoice is issued. - `cancel_at_period_end` (boolean): - `canceled_at` (timestamp | null) - `ended_at` (timestamp | null) - `trial_start` (timestamp | null) - `trial_end` (timestamp | null) - `pause_collection` (object | null) `amount_off` or `percent_off`, `duration`. - `items` (array): [Subscription items](/api/subscription-items). - `metadata` (object): - `created_at` (timestamp) - `updated_at` (timestamp) ## Create a subscription `POST /v1/subscriptions` The customer needs a default payment method for `charge_automatically`, or a kennitala for `send_invoice`. All items must share an interval and a currency, and at least one must be recurring. **Body** - `customer` (string) One or more `{ price, quantity?, credit_rollover?, included_units?, metadata? }`. - `collection_method` (enum, default charge_automatically): `charge_automatically` or `send_invoice`. - `trial_period_days` (integer): Free days before the first paid period. - `days_until_due` (integer): For `send_invoice`: days the customer has to pay each invoice. - `discount` (object): `{ amount_off?, percent_off?, duration: "once" }`. - `metadata` (object): ```ts Node.js // The customer already has a saved card (payment_method.attached fired). const subscription = await borga.subscriptions.create({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", items: [{ price: "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op", quantity: 3 }], collection_method: "charge_automatically", trial_period_days: 14, metadata: { plan: "pro" }, }); console.log(subscription.status); // "trialing" console.log(subscription.current_period_end); ``` ```bash curl curl https://api.borga.is/v1/subscriptions \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "items": [{ "price": "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op", "quantity": 3 }], "collection_method": "charge_automatically", "trial_period_days": 14 }' ``` ```json title="Response" { "id": "sub_9Qw1Er3Ty5Ui7Op9As1Df3Gh", "object": "subscription", "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "status": "trialing", "collection_method": "charge_automatically", "days_until_due": null, "current_period_start": "2026-09-07T12:00:00.000Z", "current_period_end": "2026-09-21T12:00:00.000Z", "cancel_at_period_end": false, "canceled_at": null, "ended_at": null, "trial_start": "2026-09-07T12:00:00.000Z", "trial_end": "2026-09-21T12:00:00.000Z", "pause_collection": null, "discount": null, "metadata": { "plan": "pro" }, "items": [ { "id": "si_5Zx7Cv9Bn1Mq3We5Rt7Yu9Io", "object": "subscription_item", "subscription": "sub_9Qw1Er3Ty5Ui7Op9As1Df3Gh", "price": "price_2Ab4Cd6Ef8Gh0Ij2Kl4Mn6Op", "quantity": 3, "credit_rollover": false, "included_units": null, "metadata": {}, "created_at": "2026-09-07T12:00:00.000Z", "updated_at": "2026-09-07T12:00:00.000Z" } ], "created_at": "2026-09-07T12:00:00.000Z", "updated_at": "2026-09-07T12:00:00.000Z" } ``` Errors: [`invalid_subscription`](/errors/invalid_subscription), [`invalid_price_type`](/errors/invalid_price_type), [`resource_not_found`](/errors/resource_not_found). ## Retrieve a subscription `GET /v1/subscriptions/:id` ## Update a subscription `POST /v1/subscriptions/:id` Change items through [subscription items](/api/subscription-items). **Body** - `cancel_at_period_end` (boolean) Set a discount or `null` to remove it. - `collection_method` (enum): - `days_until_due` (integer) - `metadata` (object) ## Cancel a subscription `POST /v1/subscriptions/:id/cancel` **Body** `true` keeps the subscription active until `current_period_end`, bills the final period, then cancels. `false` ends it immediately. ## Uncancel a subscription `POST /v1/subscriptions/:id/uncancel` Clears `cancel_at_period_end`. Fails with [`cannot_uncancel`](/errors/cannot_uncancel) if no cancellation is pending. ## Pause a subscription `POST /v1/subscriptions/:id/pause` Stops billing. Fails with [`cannot_pause`](/errors/cannot_pause) unless the subscription is `active` or `trialing`. ## Resume a subscription `POST /v1/subscriptions/:id/resume` Fails with [`not_paused`](/errors/not_paused) if the subscription is not paused. ## List subscriptions `GET /v1/subscriptions` **Query parameters** - `customer` (string) - `status` (enum) - `limit` (integer) - `starting_after` (string) ```ts Node.js const id = "sub_9Qw1Er3Ty5Ui7Op9As1Df3Gh"; // Change seats; Borga prorates the difference on the next invoice. const [item] = (await borga.subscriptions.retrieve(id)).items; await borga.subscriptionItems.update(item.id, { quantity: 5, proration_behavior: "create_prorations", }); // Add a one-off charge to the next invoice. await borga.invoiceItems.create({ customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", subscription: id, amount: 15000, description: "Onboarding workshop", }); // Cancel when the period ends (reversible with uncancel). await borga.subscriptions.cancel(id, { at_period_end: true }); await borga.subscriptions.uncancel(id); // Pause and resume billing. await borga.subscriptions.pause(id); await borga.subscriptions.resume(id); ``` --- # Subscription items Source: https://docs.borga.is/api/subscription-items > One price within a subscription, with a quantity. Add, change and remove items with proration. **The subscription item object** - `id` (string): Prefixed `si_`. - `object` (string): `subscription_item` - `subscription` (string): - `price` (string) Seats or units for licensed prices. - `credit_rollover` (boolean): Carry unused included units into the next period. - `included_units` (integer | null): Overrides the price's included units. - `metadata` (object): - `created_at` (timestamp) - `updated_at` (timestamp) Proration: `create_prorations` (default) records the difference for the remainder of the period and adds it to the next invoice; `always_invoice` bills it immediately; `none` skips it. ## Create a subscription item `POST /v1/subscription_items` **Body** - `subscription` (string) Must match the subscription's interval and currency. - `quantity` (integer, default 1): `create_prorations`, `always_invoice` or `none`. - `credit_rollover` (boolean): - `included_units` (integer) - `metadata` (object) ```bash curl https://api.borga.is/v1/subscription_items \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "subscription": "sub_9Qw1Er3Ty5Ui7Op9As1Df3Gh", "price": "price_7Kl9Mn1Op3Qr5St7Uv9Wx1Yz", "quantity": 1 }' ``` ## Retrieve a subscription item `GET /v1/subscription_items/:id` ## Update a subscription item `POST /v1/subscription_items/:id` **Body** - `quantity` (integer) - `proration_behavior` (enum) - `credit_rollover` (boolean) - `included_units` (integer) - `metadata` (object) ## Remove a subscription item `DELETE /v1/subscription_items/:id` **Query parameters** Credit for the unused remainder of the period. Returns `{ "deleted": true, "id": "si_…" }`. A subscription must keep at least one recurring item. --- # Invoices Source: https://docs.borga.is/api/invoices > Invoices booked in your accounting provider for payments and subscription periods. Read-only. Invoices are created by Borga when a payment succeeds, a bank invoice is issued or a subscription period ends, and mirrored to PayDay or DK+. See [Invoicing](/billing/invoicing). You cannot create or edit them through the API; add one-off charges to a subscription with [invoice items](/api/invoice-items). **The invoice object** - `id` (string): Prefixed `inv_`. - `customer` (string | null): The payment that paid it, for one-off sales and card-collected subscription invoices. - `status` (enum): `draft`, `open`, `paid`, `void` or `sync_failed`. - `subtotal` (integer): - `tax` (integer) - `total` (integer) - `amount_paid` (integer) - `currency` (string) `payday` or `dk`. - `external_invoice_id` (string | null): The provider's id. - `external_invoice_number` (string | null): The legal invoice number issued by the provider. - `pdf_url` (string | null): Provider-hosted PDF, when available. - `sync_error_message` (string | null): Set when `status` is `sync_failed`. - `external_reference` (string | null): Copied from the payment. - `billing` (object): `name`, `email`, `kennitala`, `vat_number`, `address` used on the invoice. - `lines` (array): Present when retrieving: `id`, `description`, `quantity`, `unit_amount`, `tax_rate`, `tax_amount`, `accounting_code`. - `finalized_at` (timestamp | null) - `paid_at` (timestamp | null) - `created_at` (timestamp) ## Retrieve an invoice `GET /v1/invoices/:id` ```bash curl https://api.borga.is/v1/invoices/inv_3Ef5Gh7Ij9Kl1Mn3Op5Qr7St \ -H "Authorization: Bearer sk_test_…" ``` ## List invoices `GET /v1/invoices` **Query parameters** - `status` (enum) - `limit` (integer) - `starting_after` (string) ```ts Node.js const { data, has_more } = await borga.invoices.list({ status: "paid", limit: 50 }); for (const invoice of data) { console.log(invoice.external_invoice_number, invoice.total, invoice.pdf_url); } if (has_more) { const next = await borga.invoices.list({ status: "paid", starting_after: data[data.length - 1].id, }); console.log(next.data.length); } ``` --- # Invoice items Source: https://docs.borga.is/api/invoice-items > One-off charges or credits queued for a customer's next subscription invoice. **The invoice item object** - `id` (string): Prefixed `ii_`. - `object` (string): `invoice_item` - `customer` (string): - `subscription` (string | null) Set once the item has been placed on an invoice. - `amount` (integer): Per unit. Negative for credits. - `currency` (string): Shown as the invoice line. - `quantity` (integer): - `metadata` (object) - `created_at` (timestamp) ## Create an invoice item `POST /v1/invoice_items` **Body** - `customer` (string) Per unit in the smallest currency unit. Negative amounts create credits. - `description` (string, required): Place the item on this subscription's next invoice. Must belong to `customer`. - `currency` (string, default ISK): Must match the subscription's currency when one is given. - `quantity` (integer, default 1): - `metadata` (object) ```bash curl https://api.borga.is/v1/invoice_items \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "subscription": "sub_9Qw1Er3Ty5Ui7Op9As1Df3Gh", "amount": 15000, "description": "Onboarding workshop" }' ``` Errors: [`mismatch`](/errors/mismatch), [`currency_mismatch`](/errors/currency_mismatch), [`resource_not_found`](/errors/resource_not_found). ## Retrieve an invoice item `GET /v1/invoice_items/:id` ## List invoice items `GET /v1/invoice_items` **Query parameters** - `customer` (string) - `subscription` (string) `true` returns only items not yet invoiced. - `limit` (integer) - `starting_after` (string) --- # Credit notes Source: https://docs.borga.is/api/credit-notes > Credit notes issued in your accounting provider when a payment is refunded. Read-only. A succeeded [refund](/api/refunds) against a payment whose invoice was booked in PayDay or DK+ produces a credit note there. Borga records it and emits `credit_note.created`. **The credit note object** - `id` (string): Prefixed `cn_`. - `invoice` (string): The invoice being credited. - `amount` (integer): - `currency` (string) `duplicate`, `fraudulent`, `order_change`, `product_unsatisfactory` or `other`. - `status` (enum): `pending`, `issued`, `failed` or `void`. - `accounting_provider` (enum | null): `payday` or `dk`. - `external_credit_note_id` (string | null): The provider's id. - `created_at` (timestamp): ## Retrieve a credit note `GET /v1/credit_notes/:id` ## List credit notes `GET /v1/credit_notes` Returns up to `limit` credit notes without a cursor. **Query parameters** - `invoice` (string) Up to 100. ```bash curl "https://api.borga.is/v1/credit_notes?invoice=inv_3Ef5Gh7Ij9Kl1Mn3Op5Qr7St" \ -H "Authorization: Bearer sk_test_…" ``` --- # Meters Source: https://docs.borga.is/api/meters > Turn usage events into a billable quantity per customer and period. See [Usage-based billing](/billing/usage-based) for how meters, prices and events fit together. **The meter object** - `id` (string): Prefixed `mtr_`. - `object` (string): `meter` - `name` (string): Matched against `event_name` on usage events. - `aggregate` (object): `type` (`count`, `sum`, `max`, `min`, `avg`, `unique`) and `property`. - `filter` (object | null): Metadata conditions events must match. - `unit_label` (string | null): - `unit_multiplier` (integer | null) - `archived_at` (timestamp | null) - `created_at` (timestamp) - `updated_at` (timestamp) ## Create a meter `POST /v1/meters` **Body** Up to 200 characters. - `event_name` (string, required): Up to 200 characters. - `aggregate_type` (enum, required): `count`, `sum`, `max`, `min`, `avg` or `unique`. - `aggregate_property` (string): Metadata path such as `metadata.total_tokens`. Required unless `aggregate_type` is `count`. - `filter` (object): Only events whose metadata matches count, e.g. `{ "metadata.tier": "pro" }`. - `unit_label` (string): Shown on invoices, e.g. `tokens`. - `unit_multiplier` (integer): Divide the aggregate by this before pricing. ```ts Node.js // Count API requests const requests = await borga.meters.create({ name: "API requests", event_name: "api_request", aggregate_type: "count", unit_label: "requests", }); // Sum tokens, priced per million const tokens = await borga.meters.create({ name: "LLM tokens", event_name: "llm_completion", aggregate_type: "sum", aggregate_property: "metadata.total_tokens", unit_label: "tokens", unit_multiplier: 1_000_000, }); ``` Errors: [`missing_aggregate_property`](/errors/missing_aggregate_property). ## Retrieve a meter `GET /v1/meters/:id` ## List meters `GET /v1/meters` **Query parameters** - `archived` (boolean): `true` includes archived meters. - `limit` (integer, default 25): - `starting_after` (string) ## Archive a meter `DELETE /v1/meters/:id` Archived meters stop accepting events but keep their history and remain readable. ## Meter quantity for a period `GET /v1/meters/:id/quantities` Aggregates one customer's events over a period. **Query parameters** - `customer` (string) Defaults to the customer's current billing period. - `period_end` (timestamp) ```json title="Response" { "meter": "mtr_1Zx3Cv5Bn7Mq9We1Rt3Yu5Io", "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "period_start": "2026-09-01T00:00:00.000Z", "period_end": "2026-10-01T00:00:00.000Z", "quantity": 14250, "unit_label": "requests" } ``` ## Customer meter balance `GET /v1/customer-meters/:customerId/:meterId` The live state of a customer's meter in the current period, including included units and overage. ```ts Node.js const balance = await borga.meters.balance( "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "mtr_1Zx3Cv5Bn7Mq9We1Rt3Yu5Io", ); console.log(balance.consumed_units, "of", balance.included_units); console.log("overage:", balance.overage_units); ``` ```json title="Response" { "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "meter": "mtr_1Zx3Cv5Bn7Mq9We1Rt3Yu5Io", "period_start": "2026-09-01T00:00:00.000Z", "period_end": "2026-10-01T00:00:00.000Z", "consumed_units": 7420, "included_units": 10000, "balance": 2580, "overage_units": 0 } ``` ## Customer billing state `GET /v1/customer-state/:customerId` Everything needed for a customer-facing billing page in one call: active, trialing and past-due subscriptions with their items, and every meter balance. ```json title="Response" { "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "subscriptions": [ { "id": "sub_9Qw1Er3Ty5Ui7Op9As1Df3Gh", "status": "active", "current_period_start": "2026-09-01T00:00:00.000Z", "current_period_end": "2026-10-01T00:00:00.000Z", "cancel_at_period_end": false, "items": [{ "id": "si_…", "price": "price_…", "quantity": 3 }] } ], "meters": [ { "meter": "mtr_1Zx3Cv5Bn7Mq9We1Rt3Yu5Io", "name": "API requests", "unit_label": "requests", "consumed_units": 7420, "included_units": 10000, "balance": 2580, "overage_units": 0, "period_start": "2026-09-01T00:00:00.000Z", "period_end": "2026-10-01T00:00:00.000Z" } ] } ``` --- # Usage events Source: https://docs.borga.is/api/usage-events > Report what customers consume. Events are matched to meters by name and aggregated at period end. Usage events live at `/v1/events`. They are the raw input to [meters](/api/meters), and are unrelated to webhook events, which are delivered to you rather than read from the API. **The usage event object** - `id` (string): Prefixed `ue_`. - `object` (string): `usage_event` - `event_name` (string): Resolved customer, from `customer` or `external_customer_id`. - `external_customer_id` (string | null): - `timestamp` (timestamp) - `metadata` (object) - `idempotency_key` (string | null) - `created_at` (timestamp) ## Ingest one event `POST /v1/events` **Body** Matches a meter's `event_name`. Up to 200 characters. - `customer` (string): The `cus_` id. Provide this or `external_customer_id`. - `external_customer_id` (string): Your id, matched to `Customer.external_id`. Unmatched events are stored and linked when the customer is created. - `timestamp` (timestamp): Defaults to now. Rejected if more than 60 seconds in the future. - `idempotency_key` (string): Dedup key, up to 200 characters. Repeats are dropped and counted in `duplicates`. - `metadata` (object): Values a meter's `aggregate_property` and `filter` refer to. ```ts Node.js // Single event await borga.usageEvents.create({ event_name: "api_request", customer: "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", idempotency_key: "req_01J9X2K7M3", // dedupes retries metadata: { endpoint: "/v1/chat" }, }); // Batches of up to 1000, identified by your own customer ids await borga.usageEvents.createBatch([ { event_name: "llm_completion", external_customer_id: "user_12345", timestamp: "2026-09-07T12:00:00Z", idempotency_key: "cmpl_a1", metadata: { total_tokens: 640 }, }, { event_name: "llm_completion", external_customer_id: "user_12345", timestamp: "2026-09-07T12:00:02Z", idempotency_key: "cmpl_a2", metadata: { total_tokens: 1024 }, }, ]); ``` ```bash curl curl https://api.borga.is/v1/events \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "event_name": "api_request", "customer": "cus_8Jk2Lm4Np6Qr8St0Uv2Wx4Yz", "idempotency_key": "req_01J9X2K7M3", "metadata": { "endpoint": "/v1/chat" } }' ``` ```json title="Response" { "inserted": 1, "duplicates": 0 } ``` Errors: [`invalid_timestamp`](/errors/invalid_timestamp), [`future_timestamp`](/errors/future_timestamp), [`resource_not_found`](/errors/resource_not_found) for `customer`. ## Ingest a batch `POST /v1/events/batch` **Body** - `events` (array, required): 1 to 1000 event objects with the fields above. ```bash curl https://api.borga.is/v1/events/batch \ -H "Authorization: Bearer sk_test_…" \ -H "Content-Type: application/json" \ -d '{ "events": [ { "event_name": "api_request", "external_customer_id": "user_12345", "idempotency_key": "req_1" }, { "event_name": "api_request", "external_customer_id": "user_12345", "idempotency_key": "req_2" } ] }' ``` The whole batch is accepted or rejected together; the response counts `inserted` and `duplicates`. ## List usage events `GET /v1/events` Newest first by `timestamp`. **Query parameters** - `customer` (string): - `event_name` (string) Inclusive lower bound on `timestamp`. - `to` (timestamp): Exclusive upper bound on `timestamp`. - `limit` (integer) - `starting_after` (string) --- # Event types Source: https://docs.borga.is/api/events > Every webhook event Borga sends, when it fires and what its data field contains. Events are delivered to your [webhook endpoints](/webhooks) as JSON with four fields: **The event object** - `id` (string): Unique id, prefixed `evt_`. Use it to deduplicate retried deliveries. - `type` (string): One of the types below, such as `payment.succeeded`. - `created_at` (timestamp): When the event was created. - `data` (object): The object the event is about, or a summary for lifecycle events. Listed per type below. Subscribe to specific types when you create an endpoint, or to `*` for everything. Unknown types may appear as Borga grows; ignore what you do not handle. ```json Example { "id": "evt_5Rt8Uv1Wx3Yz5Ab7Cd9Ef1Gh", "type": "payment.succeeded", "created_at": "2026-09-07T12:04:31.512Z", "data": { "id": "pay_7Hs2Kq9LmW4xZc1Vb8Ny3Rt6", "amount": 12900, "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": "4242", "wallet_type": null, "created_at": "2026-09-07T12:03:58.201Z", "updated_at": "2026-09-07T12:04:31.498Z" } } ``` ## Catalog _(EventCatalog table: see the HTML page)_ Objects referenced as "the Payment object", "the Subscription object" and so on have exactly the shape of the corresponding `GET` response. For summary payloads, fetch the object by its `id` when you need the full state.