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. |
api_key_mode_mismatch | 403 | The key's mode (test or live) does not match the resource. |
apply_live_cooldown | 400 | A live-mode application was submitted less than 15 minutes ago. |
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_not_enabled | 400 | enabled_methods includes bank_invoice but the merchant has not enabled bank invoices. Turn it on under Settings → Bank invoice. |
business_not_verified | 400 | Business verification has not completed, so live mode is not available yet. |
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_reverse_failed | 400 | The card processor definitively rejected the refund. The Refund is marked failed. |
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. |
company_kennitala_mismatch | 400 | The company kennitala in the token does not match company_kennitala. |
company_token_identity_mismatch | 400 | The Kenni company token was issued to a different user than the one signed in. |
company_token_missing_name | 400 | The Kenni company token 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. |
database_error | 500 | A transient storage error. Retry with backoff. |
expired_card | 402 | The card has expired. |
expired_session | 401 | The dashboard session token has expired. |
future_timestamp | 400 | A usage event timestamp is more than 60 seconds in the future. |
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. |
internal_error | 500 | Unexpected server error. Retry with backoff and report request_id if it persists. |
invalid_amount | 400 | The amount is not a positive integer in the smallest currency unit. |
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_company_id_token | 400 | The Kenni company token could not be verified. |
invalid_currency | 400 | The currency is not supported. Use one of the supported currencies. |
invalid_domain | 400 | A domain in allowed_redirect_domains is not a valid hostname. |
invalid_embed_origin | 400 | The embedding origin is not a valid https://host origin. |
invalid_field | 400 | A field failed validation. Check param and message. |
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_redirect_url | 400 | return_url or cancel_url is not a valid absolute URL. |
invalid_request | 400 | The request is not valid for this key type. |
invalid_session | 401 | The dashboard session token is invalid. |
invalid_subscription | 400 | The items are not a valid subscription: at least one recurring price, and all items must share interval and currency. |
invalid_timestamp | 400 | timestamp is not a valid ISO 8601 timestamp. |
invalid_token | 401 | The dashboard session token could not be verified. |
invalid_url | 400 | Webhook URLs must be public HTTPS URLs; localhost and private addresses are rejected. |
kennitala_locked | 409 | The payer kennitala was pre-supplied by the merchant and cannot change. |
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. |
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_client_secret | 401 | An embedded session was fetched without the X-Client-Secret header. |
missing_company_id_token | 400 | Applying for live mode requires a fresh Kenni company authentication token. |
missing_company_kennitala | 400 | Applying for live mode requires the company's kennitala. |
missing_customer | 400 | customer is required for this query. |
missing_field | 400 | A required field is absent. Check param for the field name. |
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_redirect_urls | 400 | A hosted session needs both return_url and cancel_url. |
no_checkout_reference | 404 | The session has no card checkout to poll. |
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. |
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. |
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_intent_authentication_failure | 402 | 3-D Secure authentication failed or was abandoned. |
payment_method_inactive | 400 | The payment method is detached, expired or failed and cannot be the default. |
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. |
pdf_not_supported | 400 | The connected accounting provider does not expose invoice PDFs. |
poll_disabled | 403 | Client-side status polling is a development-only fallback and is off in production. |
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. |
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. 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_company_id_token | 400 | The Kenni company token is older than five minutes. Authenticate again. |
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. |
unsupported_payment_method | 400 | A value in enabled_methods is not a payment method Borga supports. |
upstream_unavailable | 500 | A provider Borga depends on (card processor, accounting system) is unavailable. Retry later. |
validation_failed | 400 | Request validation failed. The message names the first failing constraint and param the field. |
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). |