error_code the Bachs API returns. When an error has a code, its doc_url links to that code’s entry here, or to the guide that explains it. For the error object shape and how to handle errors, see Errors.
Sections marked Limited Access cover features that are not enabled by default. Contact support@bachs.io to request access.
General
Errors that can occur on any endpoint.| Code | Status | Cause and resolution |
|---|---|---|
BAD_REQUEST | 400 | The request is malformed or has invalid parameters. Check it against the endpoint reference. |
UNAUTHORIZED | 401 | No Authorization header, or the key is invalid or revoked. Send a valid key. |
FORBIDDEN | 403 | The key is valid but lacks permission. Check your key’s permissions. |
NOT_FOUND | 404 | The resource does not exist in your scope. Verify the ID and environment. |
INSUFFICIENT_BALANCE | 400 | The available_balance will not cover the debit. detail states the shortfall, what was required, and what was available. Fund the balance, or wait for a pending charge to settle. |
IDEMPOTENCY_IN_PROGRESS | 409 | A request with the same Idempotency-Key is still in flight. Retry after a short delay; the winner’s response is replayed once it lands. |
CONFLICT | 409 | The request conflicts with current state, such as a duplicate. Reconcile and retry. |
VALIDATION_ERROR | 400 | One or more fields failed validation. Inspect the errors array and correct the input. |
VALIDATION_FAILED | 422 | A business rule rejected a well-formed request. Read detail for the rule. |
INVALID_REQUIREMENT_FIELD | 400 | A field submitted on POST /v1/accounts/{account_id} was rejected. Inspect errors[] (field, message, code); no field from that submission was saved, though anything else in the same call (contact details, capability requests, profile changes) was applied before the fields were validated. See payout_destination for its rejection codes. |
TOO_MANY_REQUESTS | 429 | Rate limit exceeded. Check Retry-After or X-RateLimit-Reset before retrying. |
PRECONDITION_REQUIRED | 428 | A required precondition is missing. Read detail for what to supply. |
TOTP_STEP_UP_REQUIRED | 428 | This action needs two-factor verification. Complete step-up and retry. |
IMMUTABLE_FIELD | 400 | A field that cannot change after creation was included in an update. Remove it. |
INTERNAL_SERVER_ERROR | 500 | Unexpected server-side failure. The request was not processed. Retry shortly. |
NOT_IMPLEMENTED | 501 | The operation is not yet supported. |
BAD_GATEWAY | 502 | An upstream dependency is temporarily unavailable. Retry shortly. |
SERVICE_UNAVAILABLE | 503 | The service is temporarily overloaded or in maintenance. Retry shortly. |
Payments
Encountered when initiating or processing payments.| Code | Status | Cause and resolution |
|---|---|---|
PAYMENT_ERROR | 400 | A general payment processing error. Read detail for the cause. |
PAYMENTS_NOT_ENABLED | 400 | Payments are not enabled for your organization. Complete onboarding or contact support. |
PAYMENT_METHOD_NOT_ENABLED | 400 | The selected method is not enabled. Use an enabled method or contact support. |
CARD_TOKEN_INVALID | 400 | The card token is invalid or expired. Collect the card again for a fresh token. |
CARD_VAULT_UNAVAILABLE | 500 | The card service could not be reached. Retry shortly. |
PAYMENT_METHOD_SETUP_FAILED | 500 | Payment method setup could not start. Retry shortly. |
OFF_SESSION_CHARGE_FAILED | 500 | The off-session charge could not be created. Retry, or fall back to an on-session payment. |
OFF_SESSION_NOT_SUPPORTED | 400 | This payment method cannot be charged off-session. Use an on-session flow. |
NO_SAVED_PAYMENT_METHOD | 400 | The customer has no saved card to charge. Save one on a checkout first. See Charge a saved card. |
SAVED_PAYMENT_METHOD_NOT_FOUND | 404 | No saved card with that pm_ id belongs to this customer. Cards are scoped to one customer. |
PAYMENT_METHOD_UNUSABLE | 400 | The saved card has expired or been removed. Ask the customer to save a new one. |
CHECKOUT_CANNOT_SAVE_PAYMENT_METHOD | 400 | Saving a card needs a card on the checkout. This one offers no card method, so nothing could be saved. See Why a checkout cannot save a card. |
GATEWAY_UNAVAILABLE | 500 | Our payment processing infrastructure is temporarily unreachable. Rare. Retry shortly, contact support if it persists. |
PROVIDER_ERROR | 500 | Our payment processing infrastructure returned an unexpected error. Retry shortly. |
Products & Prices
Encountered when creating or updating products and prices.| Code | Status | Cause and resolution |
|---|---|---|
PRODUCT_NOT_FOUND | 400 | A referenced product does not exist or is archived. Verify the product ID. |
PRODUCT_NO_PRICE | 400 | The product has no price in the requested currency. Add a price in that currency. |
PRODUCT_ARCHIVED | 400 | The product is archived and cannot be used for new checkouts. Unarchive or use another. |
PRICE_REQUIRED | 400 | At least one price is required. Include a price on the product. |
MINIMUM_PRICE_REQUIRED | 400 | A product must keep at least one price. You cannot remove the last one. |
DUPLICATE_CURRENCY | 400 | A price for this currency already exists. Update the existing price instead. |
UNSUPPORTED_CURRENCY | 400 | The currency is not in the active supported set. Use a supported currency. |
INTERVAL_REQUIRED | 400 | interval_count requires a positive integer when interval is set. Supply both. |
INTERVAL_NOT_ALLOWED | 400 | interval_count was provided without an interval. Add an interval or remove the count. |
INVALID_INTERVAL | 400 | interval must be one of day, week, month, or year. |
RECURRING_INTERVAL_IMMUTABLE | 400 | recurring_interval cannot change once set. Create a new product instead. |
TRIAL_REQUIRES_RECURRING | 400 | A trial is only valid on a recurring product. Remove the trial or make it recurring. |
MEDIA_LIMIT_EXCEEDED | 400 | A product supports at most 5 media items. Remove some before adding more. |
METADATA_LIMIT_EXCEEDED | 400 | metadata allows at most 20 keys, each key and value at most 500 characters. Trim it. |
ENVIRONMENT_MISMATCH | 400 | All products in a group must be in the same environment. Use one environment. |
INVALID_PRODUCT_REFERENCE | 400 | A product ID is invalid or belongs to another merchant. Verify the IDs. |
INVALID_UPLOAD_REFERENCE | 400 | An upload ID is invalid or belongs to another merchant. Verify the IDs. |
Checkout Sessions
Encountered when creating or processing checkout sessions.| Code | Status | Cause and resolution |
|---|---|---|
CHECKOUT_ERROR | 400 | A general checkout configuration or processing error. Read detail for the cause. |
CHECKOUT_PRICE_CHANGED | 409 | The price changed after the quote was issued. Fetch a fresh quote and retry. |
CUSTOM_AMOUNT_REQUIRED | 400 | A CUSTOM-priced product needs an amount in the line item. Provide one. |
FIXED_AMOUNT_OVERRIDE_NOT_ALLOWED | 400 | An amount was set for a FIXED-priced product. Remove amount from the line item. |
HETEROGENEOUS_PRICE_TYPES | 422 | All prices for a product must share one type (FIXED, CUSTOM, or FREE). |
PAYMENT_METHOD_NOT_ALLOWED | 400 | The checkout was restricted to other payment methods or currencies. Choose one the checkout offers. |
CHECKOUT_HAS_NO_PAYMENT_METHOD | 400 | Nothing can be offered on this checkout, so it was refused instead of created unpayable. Work through Why a checkout has no payment method. |
CHECKOUT_RESTRICTION_LEAVES_NO_PAYMENT_METHOD | 400 | Your payment_method_types narrowed the checkout to nothing that is available. Widen it, or see Why a checkout has no payment method. |
ACCOUNT_NOT_ACTIVATED | 400 | The account has payment methods configured but is not yet approved to accept live payments. Finish going live, or keep building against a sandbox key. |
ACCOUNT_PAYMENT_METHODS_RESTRICTED | 400 | Every payment method on the account is restricted, so no checkout can offer one. You cannot lift this from your own settings. Contact support@bachs.io. |
CART_CURRENCY_MISMATCH | 400 | All products in a multi-item checkout must share the same base currency. |
BASE_CURRENCY_NOT_HELD_BY_ORG | 422 | A recurring checkout was priced in a currency you do not hold. One-time checkouts convert and settle in any supported currency; recurring will too once renewals settle to USD, but for now price the plan in a held currency. See Charge in any currency. |
BASE_CURRENCY_NOT_COLLECTIBLE | 422 | No payment method can collect the base_currency. Crypto asset codes are refused here: an asset code names a rail, not a price. |
BASE_CURRENCY_NOT_ENABLED | 422 | The base_currency is collectible, but not enabled for your organization. Enable it in your checkout settings first. |
BASE_CURRENCY_NOT_CONVERTIBLE | 422 | We can collect the base_currency but have no rate to settle it into your settlement currency, so the checkout is refused rather than created with money that could not be paid out. |
BILLING_CURRENCY_NOT_AVAILABLE | 400 | Nothing in the checkout is priced in the requested billing_currency. Add a price in that currency, or request one of the currencies named in detail. |
BILLING_CURRENCY_HAS_NO_PAYMENT_METHOD | 400 | The checkout is priced in the requested billing_currency, but no payment method it offers can charge it. Request one of the currencies named in detail. See Pin the currency. |
Subscriptions Limited Access
Encountered when creating or managing subscriptions.| Code | Status | Cause and resolution |
|---|---|---|
SUBSCRIPTIONS_NOT_ENABLED | 403 | Subscriptions are not enabled for this account. Contact support to request access. |
NGN_SUBSCRIPTIONS_NOT_AVAILABLE | 403 | Subscriptions are billed in USD. Price the plan in USD. |
TRIALS_NOT_ENABLED | 403 | Trials are not enabled for this account. Contact support to request access. |
SUBSCRIPTION_NOT_FOUND | 404 | No subscription exists for this ID in your scope. Verify the ID. |
SUBSCRIPTION_ALREADY_CANCELED | 400 | The subscription is already canceled and cannot be canceled again. |
SUBSCRIPTION_NOT_MODIFIABLE | 400 | The subscription cannot be modified in its current status. Check its status first. |
SUBSCRIPTION_NOT_TRIALING | 400 | The action requires a trialing subscription, but this one is not in a trial. |
SUBSCRIPTION_TRIAL_ENDS_IN_PAID_PERIOD | 400 | You added a trial to an active subscription, and the trial ends before current_period_end. The customer has already paid until then, and the trial end starts a new billed period, so they would pay twice for the same days. Send a trial_end after current_period_end. |
INVALID_SUBSCRIPTION_PRICE | 400 | The price is not valid for a subscription. It must be recurring with a positive amount. |
SUBSCRIPTION_PRICE_NOT_RECURRING | 400 | A subscription needs a recurring price. You also get this when you move a subscription to a one-time product. Use a product with a billing cycle. |
SUBSCRIPTION_REQUIRES_CATALOG_PRODUCT | 400 | A subscription checkout needs exactly one recurring catalog product. Raw amounts and inline products are not supported. |
SUBSCRIPTION_METHOD_NOT_SUPPORTED | 400 | Subscription checkouts can only be paid with a card. Use a card. |
PLAN_NOT_PRICED_IN_CURRENCY | 400 | The target product has no price in the subscription’s currency. Add one or choose another plan. |
SUBSCRIPTION_PLAN_INTERVAL_MISMATCH | 400 | The new plan bills on a different cycle, for example monthly to annual, and the change can’t be made. Either the subscription has more than one item (every item bills on one cycle, so one item can’t move alone), or you sent seat_tiers (a cycle change bills a flat price at renewal). Choose a plan on the current cycle, or send the change without seat_tiers. |
SUBSCRIPTION_ITEM_REQUIRED | 400 | The subscription has more than one priced item, so a plan or seat change has to say which one. Send subscription_item_id. The error message lists the item IDs. |
SUBSCRIPTION_ITEM_NOT_FOUND | 404 | The subscription_item_id you sent is not a priced item on this subscription. Read the subscription’s items for the right ID. |
PRORATION_BEHAVIOR_NOT_SUPPORTED | 400 | The requested proration behavior is not supported yet. Use a supported value. |
SUBSCRIPTION_CHANGE_ALREADY_PENDING | 400 | Another change is staged on this subscription and has not taken effect yet: an upgrade waiting for its charge, or a billing cycle change waiting for renewal. While one is staged, plan changes are refused, so the staged change cannot overwrite yours when it applies. Seat changes are refused too while an upgrade waits for its charge. A seat change made while a billing cycle change waits goes through, and the waiting change moves to the new seat count. Read pending_update, then wait for it or cancel it with DELETE /v1/subscriptions/{id}/pending_update. |
SUBSCRIPTION_CHANGE_IN_PROGRESS | 409 | Another change to this subscription was still running, usually one charging a card, and did not finish within 2 seconds. Nothing was changed or charged. Retry after a few seconds. If you see this often, send changes to one subscription one at a time. |
Payouts
Encountered when sending payouts or managing payout destinations.| Code | Status | Cause and resolution |
|---|---|---|
PAYOUTS_NOT_ENABLED | 400 | Withdrawals are not enabled for this organization. Complete the account’s withdrawal requirements; contact support if the error persists. |
PAYOUT_CURRENCY_NOT_ENABLED | 400 | The requested payout route is disabled for this account. Payouts are available to all Bachs users; if this occurs for a route listed in Payout currencies and methods, contact support with the error details. |
PAYOUT_SOURCE_CURRENCY_UNSUPPORTED | 400 | The chosen source balance cannot fund this route. International bank and mobile money payouts use your USD balance. |
PAYOUT_RECIPIENT_INCOMPLETE | 400 | A payout to mobile money or to a bank outside Nigeria needs the account’s name, email and address. detail names what is missing, for example “This account is missing its address (street, city and country).” Add it to the account and retry. |
INTERNATIONAL_PAYOUTS_NOT_ENABLED | 400 | A connected account was to be paid out to mobile money or to a bank outside Nigeria, and international payouts are off for it. Its platform requests payouts with international: true for the account. See International payouts. |
DESTINATION_NOT_FOUND | 404 | No destination with that id belongs to this account. Check the id. |
DESTINATION_PENDING_REVIEW | 400 | The destination has not cleared review, so it cannot receive money yet. Wait for is_usable to be true. |
DESTINATION_REJECTED | 400 | The destination was rejected in review and never becomes usable. Register a new one. |
DESTINATION_TYPE_REQUIRED | 400 | The currency is served by more than one payout rail, so the type cannot be inferred. Send type explicitly. |
CURRENCY_NOT_SUPPORTED | 400 | The requested payout currency and destination type have no supported rail. See Supported currencies. |
AMOUNT_REQUIRED | 400 | A same-currency payout needs an amount. |
AMOUNT_NOT_ALLOWED_WITH_QUOTE | 400 | A quoted payout carries no amount, because the quote already fixes both sides. Send one or the other. |
QUOTE_REQUIRED | 400 | The destination’s currency differs from the balance being debited. Create a quote and pass quote_id. |
QUOTE_NOT_APPLICABLE | 400 | A quote was passed to a same-currency payout, which converts nothing. Drop quote_id. |
QUOTE_NOT_FOUND | 400 | The quote does not exist, or belongs to another organization. Create a new one. |
QUOTE_EXPIRED | 400 | The quote has lapsed. Quotes are short-lived; create one immediately before the payout. |
QUOTE_DESTINATION_MISMATCH | 400 | The quote was created for a different currency pair than this destination’s. Quote the pair you are paying. |
PAYOUT_DESTINATION_NOT_FOUND | 404 | No destination with that id belongs to this account. Returned when choosing the destination a payout schedule uses. |
PAYOUT_DESTINATION_NOT_APPROVED | 400 | Only an approved destination can be a payout schedule’s default. Wait for review to clear. |
PAYOUT_DESTINATION_INACTIVE | 400 | A deleted destination cannot be a payout schedule’s default. Register the account again and promote the new one. |
WITHDRAWAL_LIMIT_EXCEEDED | 400 | The amount exceeds your single-withdrawal limit. details has currency, requested_amount and max_allowed, plus requested_amount_usd and max_allowed_usd. See Withdrawal limit details. Lower the amount. |
DAILY_WITHDRAWAL_LIMIT_EXCEEDED | 400 | The amount would exceed your daily cap. details has currency, requested_amount, max_allowed and total_today, plus requested_amount_usd, max_allowed_usd and total_today_usd. See Withdrawal limit details. Retry within the cap. |
INSUFFICIENT_BALANCE | 400 | available_balance will not cover the payout. Compare it against total_debited (what the destination receives plus the fee), not against amount. See General for the shortfall the response states. |
Limit errors include a
details object alongside error_code and detail. Parse it to show remaining allowance and cap amounts directly to your users.Withdrawal limit details
currency, requested_amount, max_allowed and total_today are in the currency the limit is set in. total_today is only on the daily error.
If your account has a limit for the payout currency, the payout is judged against that limit only. The amounts are in that currency, and the _usd fields are null.
If it has no limit for that currency, the check is in USD. currency is "USD", and the _usd fields are filled.
For example, an NGN payout of 12,000,000 against a 10,000,000 limit:
{
"detail": "Withdrawal limit exceeded. Requested: NGN 12,000,000.00, maximum allowed: NGN 10,000,000.00",
"error_code": "WITHDRAWAL_LIMIT_EXCEEDED",
"doc_url": "https://docs.bachs.io/api-reference/error-reference#payouts",
"details": {
"requested_amount_usd": null,
"max_allowed_usd": null,
"currency": "NGN",
"requested_amount": "12000000.00",
"max_allowed": "10000000.00"
}
}
Deposits
Encountered when collecting payments from customers.| Code | Status | Cause and resolution |
|---|---|---|
DEPOSIT_LIMIT_EXCEEDED | 400 | The payment exceeds the limit for its payment method, or your account maximum for the currency. details has requested_amount, max_allowed_amount, and currency. Lower the amount, use a method with a higher limit, or see Deposit limits. |
UNSUPPORTED_DEPOSIT_CURRENCY | 400 | The currency is not a supported deposit currency. Use a supported currency. |
Virtual accounts
Encountered when creating or reading an account’s fixed account number. See Accept bank transfers with a virtual account.| Code | Status | Cause and resolution |
|---|---|---|
VIRTUAL_ACCOUNT_CURRENCY_NOT_SUPPORTED | 400 | We do not issue virtual accounts in that currency. Request NGN, the only supported currency today. |
VIRTUAL_ACCOUNT_REFUSED | 400 | The issuing bank refused to create the number, or was down. The message carries the reason. If it says “The bank could not make an account number right now”, wait a minute and retry, or send a different bank_code: a retry never creates a second number. Otherwise fix what the reason names and retry, or contact support if it isn’t clear. |
VIRTUAL_ACCOUNT_BANK_UNAVAILABLE | 400 | The bank you chose with bank_code is switched off for now. Pick another bank, or leave bank_code out and we pick one. See Choose the bank. |
VALIDATION_ERROR | 400 | bank_code is not one of 103, 090645 or 110072. |
FORBIDDEN | 403 | Your key lacks virtual_accounts:read or virtual_accounts:write, or the account’s virtual_accounts capability is not active yet. Check the key’s permissions, then wait for capability.updated to report the capability as active. |
VALIDATION_FAILED | 422 | The account representative has no BVN on file. The response includes missing_fields: ["bvn"]. Submit the representative’s BVN through the account’s requirements, then retry. |
NOT_FOUND | 404 | On GET, the account has no virtual account in that currency, so create one with POST /v1/virtual-accounts. On POST, virtual accounts are not enabled for this account. Contact support with the account ID. |
Connect
Encountered when you create, change or onboard connected accounts. See Accounts.| Code | Status | Cause and resolution |
|---|---|---|
invalid_configuration | 400 | configuration names a persona that does not exist. Use merchant or recipient. |
capability_configuration_mismatch | 400 | A capability is nested under the wrong persona. Check which persona it belongs to. See Capabilities. |
capability_active | 400 | You tried to withdraw a capability that is live. Only our team can turn it off. Contact support. |
capability_disabled | 400 | You tried to withdraw a capability our team turned off. Contact support. |
capability_option_not_supported | 400 | An option was sent on a capability that does not take it. Only payouts takes international. Remove it from the other capability. |
capability_option_platform_only | 400 | The account itself tried to change international. Only its platform can. Send the request with your platform key. |
capability_unrequest_unsupported | 400 | "requested": false was sent by someone other than the account’s platform. Only the platform can withdraw a capability. |
invalid_include | 400 | include names a block that does not exist. Check the spelling. |
account_write_in_progress | 409 | Another write to the same account is still saving. Wait a moment and send it again. |
invalid_dashboard_mode | 400 | dashboard is not a mode you can use. Send none, light or full. See Give an account a dashboard. |
unknown_dashboard_feature | 400 | dashboard_features names a switch that does not exist, or names view_payments, which is always on. |
invalid_dashboard_feature_value | 400 | dashboard_features is not an object, or a value in it is not true or false. |
dashboard_cannot_be_lowered | 400 | You tried to lower an account’s access mode. Contact support. |
dashboard_platform_only | 400 | An account tried to change its own access. Only its platform can. |
login_links_need_light | 400 | You asked for a login link for an account that is not in light mode. Raise it to light first. |
dashboard_invite_needs_full | 400 | You asked for a dashboard invite for an account that is not in full mode. Raise it to full first. |
dashboard_invite_needs_email | 400 | The account has no contact email to send the dashboard invite to. Set one first. |
dashboard_access_already_active | 400 | The person behind the account already accepted the dashboard invite. |
dashboard_invite_conflict | 409 | Someone on the account’s team already invited this email, or it is already on the team. |
login_email_is_platform_member | 403 | The account’s contact email belongs to someone on your own team, so we will not send it a login code. Change the contact email to the person’s own. |
CONNECTED_ACCOUNT_REQUIREMENTS_NOT_FOUND | 400 | An update link was asked for an account with nothing to update. Use an onboarding link. |
CONNECTED_ACCOUNT_LIVENESS_NOT_NEEDED | 400 | A liveness link was asked for an account that does not need the selfie video check. Send an onboarding or update link instead. |
CONNECTED_ACCOUNT_NOT_ELIGIBLE_FOR_LINK | 400 | The account is suspended or rejected, so it cannot be sent through onboarding. |
CONNECTED_ACCOUNT_PLATFORM_SUSPENDED | 403 | Your platform is suspended. Resolve that before making links for its accounts. |
INVALID_REQUIREMENT_FIELD and code account_name_mismatch. See The payout_destination shape.
Quotes Limited Access
Encountered when requesting conversion quotes.| Code | Status | Cause and resolution |
|---|---|---|
QUOTE_ERROR | 400 | The conversion quote could not be generated. Check the currency pair and amount are valid. |

