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.

Error responsejson
{
  "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/<code>, 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

typeHTTPMeaningRetry?
invalid_request_error400The request was malformed or refers to an object that does not exist.No. Fix the request.
authentication_error401The API key is missing, malformed or revoked.No. Check the key.
permission_error403The key or role is not allowed to do this, for example a live key before live mode is enabled.No.
payment_error402The card or processor declined.Depends on the code.
idempotency_error409An Idempotency-Key was reused incorrectly or is still in flight.No. Use a new key.
rate_limit_error429Too many requests.Yes, after Retry-After.
api_error500Something failed on Borga's side or at a provider.Yes, with backoff.

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.

Node.jsts
import { Borga, BorgaError } from "@borga/node";

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

codeHTTPMeaning
accounting_link_required400Bank invoices require a connected accounting provider. Connect PayDay under Settings → Accounting system.
accounting_not_connected400The merchant has no connected accounting provider.
already_applied400A live-mode application is already pending.
already_live400Live mode is already enabled for this merchant.
already_member400The invited user already belongs to this merchant.
amount_too_large400The amount exceeds what is allowed, for example a refund larger than the refundable balance.
amount_too_small400The amount is below the minimum for this operation, for example a refund of 0.
approval_required403A live refund or charge requested through an AI assistant is above the merchant's approval threshold or daily cap for that connection. details.approval carries the approval id, the dashboard URL where an owner or admin decides, and when it expires. Approve the request in the dashboard, then let the assistant retry with the approval id. Owners and admins set the threshold and cap under Settings, Connected apps.
attempt_in_progress400A payment attempt for this invoice is running, or was sent and is waiting for its result. Refresh to see it.
bank_invoice_failed400The accounting provider failed to register the bank claim.
bank_invoice_hosted_only400Bank invoices are only available in hosted checkout, not embedded.
bank_invoice_isk_only400Bank invoices (krafa) are only available for ISK: the session's payment, or the subscription's prices, are in another currency. Offer bank invoice on ISK payments only, or collect the subscription with charge_automatically.
bank_invoice_not_enabled400enabled_methods includes bank_invoice but the merchant has not enabled bank invoices. Turn it on under Settings → Bank invoice.
bank_invoice_unavailable400The connected accounting provider reports that the company cannot issue bank claims yet. For PayDay, activate claim collection on the company in PayDay, then turn bank invoices on again.
cannot_pause400Only active or trialing subscriptions can be paused.
cannot_remove_owner400The merchant owner cannot be removed.
cannot_uncancel400The subscription is not scheduled to cancel at period end.
card_declined402The card issuer declined the payment.
card_payment_charge_failed400The card processor rejected the off-session charge outright. The Payment is marked failed.
card_payment_reverse_failed400The card processor definitively rejected the refund. The Refund is marked failed. Right after a charge the processor may not have the payment on record yet; the message says so. Create the refund again in a few minutes.
card_payment_reverse_uncertain502The processor did not confirm the refund. The Refund stays pending; do not retry blindly. Poll the refund or wait for payment.refunded.
card_payment_session_failed400The card processor rejected the session. Rare; retry, and contact Borga if it persists.
card_update_unavailable400The card can't be replaced from the customer portal because the customer has no email address to bind the new card to. Set an email on the customer, then mint a new portal link.
checkout_in_progress400The customer is paying this invoice in the customer portal right now. Wait for that checkout to finish or expire.
company_kennitala_mismatch400The procurator confirmed a different company than the merchant's kennitala.
company_token_missing_name400The electronic ID login for the company does not carry a company name.
connection_failed400The accounting provider rejected the credentials.
currency_mismatch400The item's currency differs from the subscription's currency.
customer_email_locked409The payer email was locked by the merchant or at checkout and cannot change.
customer_kennitala_required400A send_invoice subscription collects by bank claim, which is registered against the customer's kennitala, and the customer has none. Add the kennitala to the customer first.
email_delivery_failed503The email could not be sent: to the procurator, nothing was submitted; for an invoice, it was not sent. Try again.
expired_card402The card has expired.
future_timestamp400A usage event timestamp is more than 60 seconds in the future.
id_token_missing_actor400The company login on the live application does not say who is acting for the company (no actor kennitala or name).
id_token_missing_name400The electronic ID login does not carry a name.
idempotency_key_in_use409A request with this Idempotency-Key is still being processed. Wait for the original request to finish and retry with the same key, or use a new key for a different request.
idempotency_key_payload_mismatch409This Idempotency-Key was already used with a different request body or route. Use a fresh key for each distinct request.
incorrect_cvc402The card's security code was rejected.
insufficient_funds402The issuer reported insufficient funds.
insufficient_permissions403Your role on this merchant does not allow the action.
insufficient_scope403The OAuth connection behind this access token was not granted the permission the endpoint needs. The WWW-Authenticate header names the scope. Reconnect the assistant and tick the missing permission on the consent screen.
internal_error500Unexpected server error. Retry with backoff and report request_id if it persists.
invalid_api_key401The API key is malformed, unknown or revoked.
invalid_client_secret401The client_secret does not match this session.
invalid_domain400A domain in allowed_redirect_domains is not a valid hostname.
invalid_field400A field failed validation. Check param and message.
invalid_id_token400The electronic ID login on the live application could not be verified.
invalid_json400The request body could not be parsed as JSON. The message carries the parser's position. Check for trailing commas and that Content-Type is application/json.
invalid_kennitala400The kennitala failed the mod-11 checksum. Kennitala is 10 digits, optionally written as 000000-0000.
invalid_origin400An origin must be scheme and host only, for example https://shop.is, with no path.
invalid_param400A parameter is out of range. Check param.
invalid_period400period_start or period_end is not a valid ISO 8601 timestamp.
invalid_price_type400A price used for a subscription must have type: "recurring".
invalid_request400The request is not valid for this key type.
invalid_subscription400The items are not a valid subscription: at least one recurring price, and all items must share interval and currency.
invalid_tax_rate400A line item's tax_rate is outside 0 to 10000. tax_rate is in basis points, so 24% is 2400.
invalid_timestamp400timestamp is not a valid ISO 8601 timestamp.
invalid_token401The session token or OAuth access token could not be verified, has expired, or was revoked.
invalid_url400Webhook URLs must be public HTTPS URLs; localhost and private addresses are rejected.
invoice_item_invoiced400The one-off charge is already on an invoice and can no longer be removed.
invoice_not_chargeable400The invoice is not charged automatically: it has no subscription, or the subscription collects by send_invoice.
invoice_not_open400Only open invoices can be charged again or marked paid.
invoice_not_payable400The invoice can't be paid from the customer portal: it isn't open, is already partly paid, or its subscription collects by bank invoice.
invoice_not_sendable400Only open and paid invoices can be emailed to the payer.
invoice_not_voidable400Only open invoices, and ones that failed to sync without collecting money, can be voided. Refund the payment of a paid invoice instead.
invoice_recently_sent400The invoice was emailed less than a minute ago.
kennitala_locked409The payer kennitala was pre-supplied by the merchant and cannot change.
kennitala_mismatch400The person who confirmed the live application is not the individual the merchant trades as.
kennitala_type_mismatch400A personal kennitala was given for a company, or a company kennitala for an individual. On a live application: the procurator logged in as themselves for a company, or on behalf of a company for an individual.
line_items_amount_mismatch400amount does not equal the total computed from line_items. Omit amount and let Borga derive it, or send the total of every line including tax.
live_application_claimed400Someone else has already confirmed this live application with their electronic ID, and only they can complete it. Have them finish it, or send a new link from Settings → Live environment in the dashboard.
live_application_link_invalid404The procurator's live-application link has expired or was replaced by a newer one. A link whose application was sent keeps working and says so. Send a new one from Settings → Live environment in the dashboard.
live_application_not_confirmed400The answers to the live application came without the applicant_token of its latest electronic ID confirmation: nobody has confirmed yet, or the answers come from another browser than the one that confirmed. Confirm first; the form asks for it as its first step, and again in a new tab or device.
live_mode_not_enabled403A live key was used before the merchant was approved for live mode. Use test keys until Settings → Live environment shows live mode as enabled.
meter_already_subscribed400The customer already has a live subscription item on this meter, so its usage events would be billed twice. Cancel the existing subscription first, or add the price to it instead of creating a second subscription.
mismatch400The subscription does not belong to customer.
missing_aggregate_property400aggregate_property is required for every aggregation except count.
missing_amount400Provide amount (to create a Payment) or payment (to attach an existing one).
missing_api_key401No Authorization: Bearer header was sent. Send your secret key as Authorization: Bearer sk_test_….
missing_billing_email400The invoice has no billing email and its customer has no email, so there is nowhere to send it. Set an email on the customer.
missing_client_secret401An embedded session was fetched without the X-Client-Secret header.
missing_customer400customer is required for this query.
missing_field400A required field is absent. Check param for the field name.
missing_kennitala400Applying for live mode requires the merchant's kennitala. Add it under Settings in the dashboard.
missing_merchant400A dashboard request did not include the X-Merchant-Id header.
missing_meter400A metered recurring price needs meter.
missing_origin400An embedded session created with a secret key needs origin, the https://host of the page that opens checkout.
missing_processor_reference400The payment has no processor reference to refund against. Contact Borga.
missing_procuration400The person who confirmed the live application does not hold procuration (prókúra) for the company.
missing_redirect_urls400A hosted session got only one of return_url and cancel_url. Give both, or neither to keep the payer on the confirmation page.
no_checkout_reference404The session has no card checkout to confirm the result against.
no_external_invoice400The invoice has not been created in the accounting provider yet.
no_merchant_access401The signed-in user does not belong to the merchant in X-Merchant-Id.
no_unknown_charge400Only an invoice whose last charge never reported back can be marked as charged.
not_authenticated401A dashboard endpoint was called without a session token.
not_paused400The subscription is not paused.
not_retryable400Only invoices in status sync_failed can be re-synced.
not_trialing400The subscription is not trialing, so there is no trial to end or extend.
origin_not_allowed400The browser's Origin is not in the publishable key's allowed_origins. Add the origin to the key under Developers in the dashboard.
payment_method_inactive400The payment method is detached, expired or failed, so it cannot be charged or made the default.
payment_method_not_chargeable400The saved card cannot be charged off-session: it has no stored processor token, or it was saved on a different card terminal. Collect the card again through a checkout session with save_payment_method: true.
payment_not_available400The Payment referenced by payment is not in status created, so it cannot get a new session.
payment_not_refundable400Only payments in status succeeded or processing can be refunded, and a processing payment only in full. For a partial refund, wait until the payment is succeeded.
payment_outcome_unknown400An earlier charge for this invoice never reported back, so Borga can't tell whether the card was charged. Automatic retries for the invoice stop until you confirm. Check the payment with the card processor. If it did not go through, retry with explicit confirmation: "It wasn't charged, retry" in the dashboard, or accept_unknown_outcome: true on the retry request. If it did go through, or the customer tells you their card was charged, don't retry: choose "It was charged" in the dashboard, which records the payment and marks the invoice paid. A late result from the card processor settles the invoice on its own too.
pdf_not_supported400The connected accounting provider does not expose invoice PDFs.
portal_consumed403A single-use customer portal link was already opened.
portal_expired403The customer portal link has expired.
portal_revoked403The customer portal link was revoked.
processing_error402The card processor returned a processing error. Safe to retry once.
provider_error400The accounting provider returned an error.
provider_no_bank_invoice400The connected accounting provider cannot issue bank claims. PayDay is required.
publishable_key_not_allowed403A publishable key was used for something only secret keys may do. Publishable keys can only create embedded sessions. Use a secret key from your server.
rate_limit_exceeded429Too many requests in the current window. Wait for Retry-After seconds, then retry.
redirect_url_not_allowed400return_url or cancel_url is not under an allowed redirect domain, or is not HTTPS. Add the domain under Settings → Account in the dashboard. In test mode http://localhost is always allowed.
request_too_large413The request body is over the 1 MB limit. Send fewer events per batch, or less metadata per event.
resource_already_exists400An object with the same unique attribute already exists.
resource_in_wrong_state400The object cannot perform this action in its current status.
resource_missing400The starting_after cursor does not reference an object in this list. Use the id of the last object from the previous page.
resource_not_found400The referenced object does not exist in this merchant and mode. Returned as 404 when the id is in the URL and as 400 when a request body refers to it. Check the id and that you are using a key for the right mode (test vs. live).
session_not_open400The payment session is no longer open; it completed or expired.
stale_id_token400The electronic ID login on the live application is older than five minutes. Confirm again.
subscription_canceled400The subscription is canceled, so its items and next invoice can no longer change.
subscription_requires_save_payment_method400A session with subscription cannot set save_payment_method: false; subscriptions always save the card.
sync_failed400Re-syncing the invoice to the accounting provider failed again.
terms_not_accepted400The current Terms of Service must be accepted before this action.
validation_failed400Request validation failed. The message names the first failing constraint and param the field, with array items indexed as events[3].timestamp.
wrong_auth_method401An API key was sent to a dashboard-only endpoint. Dashboard endpoints (webhook endpoints, API keys, merchant settings) are managed in the dashboard UI.
wrong_connection_method400This provider connects with OAuth, not a pasted token (or vice versa).