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": {
"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
| type | HTTP | Meaning | Retry? |
|---|---|---|---|
invalid_request_error | 400 | The request was malformed or refers to an object that does not exist. | No. Fix the request. |
authentication_error | 401 | The API key is missing, malformed or revoked. | No. Check the key. |
permission_error | 403 | The key or role is not allowed to do this, for example a live key before live mode is enabled. | No. |
payment_error | 402 | The card or processor declined. | Depends on the code. |
idempotency_error | 409 | An Idempotency-Key was reused incorrectly or is still in flight. | No. Use a new key. |
rate_limit_error | 429 | Too many requests. | Yes, after Retry-After. |
api_error | 500 | Something 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.
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
| code | HTTP | Meaning |
|---|---|---|
accounting_link_required | 400 | Bank invoices require a connected accounting provider. Connect PayDay under Settings → Accounting system. |
accounting_not_connected | 400 | The merchant has no connected accounting provider. |
already_applied | 400 | A live-mode application is already pending. |
already_live | 400 | Live mode is already enabled for this merchant. |
already_member | 400 | The invited user already belongs to this merchant. |
amount_too_large | 400 | The amount exceeds what is allowed, for example a refund larger than the refundable balance. |
amount_too_small | 400 | The amount is below the minimum for this operation, for example a refund of 0. |
approval_required | 403 | A 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_progress | 400 | A payment attempt for this invoice is running, or was sent and is waiting for its result. Refresh to see it. |
bank_invoice_failed | 400 | The accounting provider failed to register the bank claim. |
bank_invoice_hosted_only | 400 | Bank invoices are only available in hosted checkout, not embedded. |
bank_invoice_isk_only | 400 | Bank 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_enabled | 400 | enabled_methods includes bank_invoice but the merchant has not enabled bank invoices. Turn it on under Settings → Bank invoice. |
bank_invoice_unavailable | 400 | The 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_pause | 400 | Only active or trialing subscriptions can be paused. |
cannot_remove_owner | 400 | The merchant owner cannot be removed. |
cannot_uncancel | 400 | The subscription is not scheduled to cancel at period end. |
card_declined | 402 | The card issuer declined the payment. |
card_payment_charge_failed | 400 | The card processor rejected the off-session charge outright. The Payment is marked failed. |
card_payment_reverse_failed | 400 | The 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_uncertain | 502 | The 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_failed | 400 | The card processor rejected the session. Rare; retry, and contact Borga if it persists. |
card_update_unavailable | 400 | The 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_progress | 400 | The customer is paying this invoice in the customer portal right now. Wait for that checkout to finish or expire. |
company_kennitala_mismatch | 400 | The procurator confirmed a different company than the merchant's kennitala. |
company_token_missing_name | 400 | The electronic ID login for the company does not carry a company name. |
connection_failed | 400 | The accounting provider rejected the credentials. |
currency_mismatch | 400 | The item's currency differs from the subscription's currency. |
customer_email_locked | 409 | The payer email was locked by the merchant or at checkout and cannot change. |
customer_kennitala_required | 400 | A 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_failed | 503 | The email could not be sent: to the procurator, nothing was submitted; for an invoice, it was not sent. Try again. |
expired_card | 402 | The card has expired. |
future_timestamp | 400 | A usage event timestamp is more than 60 seconds in the future. |
id_token_missing_actor | 400 | The company login on the live application does not say who is acting for the company (no actor kennitala or name). |
id_token_missing_name | 400 | The electronic ID login does not carry a name. |
idempotency_key_in_use | 409 | A 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_mismatch | 409 | This Idempotency-Key was already used with a different request body or route. Use a fresh key for each distinct request. |
incorrect_cvc | 402 | The card's security code was rejected. |
insufficient_funds | 402 | The issuer reported insufficient funds. |
insufficient_permissions | 403 | Your role on this merchant does not allow the action. |
insufficient_scope | 403 | The 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_error | 500 | Unexpected server error. Retry with backoff and report request_id if it persists. |
invalid_api_key | 401 | The API key is malformed, unknown or revoked. |
invalid_client_secret | 401 | The client_secret does not match this session. |
invalid_domain | 400 | A domain in allowed_redirect_domains is not a valid hostname. |
invalid_field | 400 | A field failed validation. Check param and message. |
invalid_id_token | 400 | The electronic ID login on the live application could not be verified. |
invalid_json | 400 | The 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_kennitala | 400 | The kennitala failed the mod-11 checksum. Kennitala is 10 digits, optionally written as 000000-0000. |
invalid_origin | 400 | An origin must be scheme and host only, for example https://shop.is, with no path. |
invalid_param | 400 | A parameter is out of range. Check param. |
invalid_period | 400 | period_start or period_end is not a valid ISO 8601 timestamp. |
invalid_price_type | 400 | A price used for a subscription must have type: "recurring". |
invalid_request | 400 | The request is not valid for this key type. |
invalid_subscription | 400 | The items are not a valid subscription: at least one recurring price, and all items must share interval and currency. |
invalid_tax_rate | 400 | A line item's tax_rate is outside 0 to 10000. tax_rate is in basis points, so 24% is 2400. |
invalid_timestamp | 400 | timestamp is not a valid ISO 8601 timestamp. |
invalid_token | 401 | The session token or OAuth access token could not be verified, has expired, or was revoked. |
invalid_url | 400 | Webhook URLs must be public HTTPS URLs; localhost and private addresses are rejected. |
invoice_item_invoiced | 400 | The one-off charge is already on an invoice and can no longer be removed. |
invoice_not_chargeable | 400 | The invoice is not charged automatically: it has no subscription, or the subscription collects by send_invoice. |
invoice_not_open | 400 | Only open invoices can be charged again or marked paid. |
invoice_not_payable | 400 | The 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_sendable | 400 | Only open and paid invoices can be emailed to the payer. |
invoice_not_voidable | 400 | Only open invoices, and ones that failed to sync without collecting money, can be voided. Refund the payment of a paid invoice instead. |
invoice_recently_sent | 400 | The invoice was emailed less than a minute ago. |
kennitala_locked | 409 | The payer kennitala was pre-supplied by the merchant and cannot change. |
kennitala_mismatch | 400 | The person who confirmed the live application is not the individual the merchant trades as. |
kennitala_type_mismatch | 400 | A 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_mismatch | 400 | amount 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_claimed | 400 | Someone 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_invalid | 404 | The 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_confirmed | 400 | The 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_enabled | 403 | A 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_subscribed | 400 | The 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. |
mismatch | 400 | The subscription does not belong to customer. |
missing_aggregate_property | 400 | aggregate_property is required for every aggregation except count. |
missing_amount | 400 | Provide amount (to create a Payment) or payment (to attach an existing one). |
missing_api_key | 401 | No Authorization: Bearer header was sent. Send your secret key as Authorization: Bearer sk_test_…. |
missing_billing_email | 400 | The 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_secret | 401 | An embedded session was fetched without the X-Client-Secret header. |
missing_customer | 400 | customer is required for this query. |
missing_field | 400 | A required field is absent. Check param for the field name. |
missing_kennitala | 400 | Applying for live mode requires the merchant's kennitala. Add it under Settings in the dashboard. |
missing_merchant | 400 | A dashboard request did not include the X-Merchant-Id header. |
missing_meter | 400 | A metered recurring price needs meter. |
missing_origin | 400 | An embedded session created with a secret key needs origin, the https://host of the page that opens checkout. |
missing_processor_reference | 400 | The payment has no processor reference to refund against. Contact Borga. |
missing_procuration | 400 | The person who confirmed the live application does not hold procuration (prókúra) for the company. |
missing_redirect_urls | 400 | A 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_reference | 404 | The session has no card checkout to confirm the result against. |
no_external_invoice | 400 | The invoice has not been created in the accounting provider yet. |
no_merchant_access | 401 | The signed-in user does not belong to the merchant in X-Merchant-Id. |
no_unknown_charge | 400 | Only an invoice whose last charge never reported back can be marked as charged. |
not_authenticated | 401 | A dashboard endpoint was called without a session token. |
not_paused | 400 | The subscription is not paused. |
not_retryable | 400 | Only invoices in status sync_failed can be re-synced. |
not_trialing | 400 | The subscription is not trialing, so there is no trial to end or extend. |
origin_not_allowed | 400 | The 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_inactive | 400 | The payment method is detached, expired or failed, so it cannot be charged or made the default. |
payment_method_not_chargeable | 400 | The 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_available | 400 | The Payment referenced by payment is not in status created, so it cannot get a new session. |
payment_not_refundable | 400 | Only 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_unknown | 400 | An 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_supported | 400 | The connected accounting provider does not expose invoice PDFs. |
portal_consumed | 403 | A single-use customer portal link was already opened. |
portal_expired | 403 | The customer portal link has expired. |
portal_revoked | 403 | The customer portal link was revoked. |
processing_error | 402 | The card processor returned a processing error. Safe to retry once. |
provider_error | 400 | The accounting provider returned an error. |
provider_no_bank_invoice | 400 | The connected accounting provider cannot issue bank claims. PayDay is required. |
publishable_key_not_allowed | 403 | A 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_exceeded | 429 | Too many requests in the current window. Wait for Retry-After seconds, then retry. |
redirect_url_not_allowed | 400 | return_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_large | 413 | The request body is over the 1 MB limit. Send fewer events per batch, or less metadata per event. |
resource_already_exists | 400 | An object with the same unique attribute already exists. |
resource_in_wrong_state | 400 | The object cannot perform this action in its current status. |
resource_missing | 400 | The starting_after cursor does not reference an object in this list. Use the id of the last object from the previous page. |
resource_not_found | 400 | The 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_open | 400 | The payment session is no longer open; it completed or expired. |
stale_id_token | 400 | The electronic ID login on the live application is older than five minutes. Confirm again. |
subscription_canceled | 400 | The subscription is canceled, so its items and next invoice can no longer change. |
subscription_requires_save_payment_method | 400 | A session with subscription cannot set save_payment_method: false; subscriptions always save the card. |
sync_failed | 400 | Re-syncing the invoice to the accounting provider failed again. |
terms_not_accepted | 400 | The current Terms of Service must be accepted before this action. |
validation_failed | 400 | Request validation failed. The message names the first failing constraint and param the field, with array items indexed as events[3].timestamp. |
wrong_auth_method | 401 | An 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_method | 400 | This provider connects with OAuth, not a pasted token (or vice versa). |