Skip to main content

Added

  • Global Payouts through the existing payout API. Bank destinations support NGN, GHS, KES, TZS, UGX, GBP, USD, CAD and EUR, with mobile money in GHS, KES and TZS and supported stablecoin wallets. Global Payouts is available to all Bachs users; account verification, an active payouts capability, destination approval and sufficient available funds still apply. These withdrawals are to the account holder’s own destinations. Register destinations with POST /v1/payouts/destinations and check is_usable before requesting a payout. Set the destination type explicitly for bank or mobile money destinations, and choose a scheme for USD and CAD bank destinations. See Create a payout destination. International bank and mobile money routes debit the available USD balance. For a different destination currency, request a conversion quote and send its quote_id with the destination ID, without amount, when creating the payout. USD-to-USD payouts use amount without a conversion quote. A successful create response confirms acceptance, not delivery; track the payout until completed or failed. For connected accounts, use X-Account-Id with that account’s own balance and approved destination. Add scheme-based international bank destinations through the destination API rather than the inline onboarding payout_destination field. See Global Payouts and International bank payouts.
  • enabled on PATCH /v1/webhooks/endpoints/{endpoint_id}. Send false to stop all deliveries to an endpoint, or true to turn it back on. Before this, the only way to stop deliveries from the API was to delete the endpoint. Turning an endpoint on clears its failure history. Events from while it was off are not sent, so resend the ones you need.
  • disabled_at and disabled_reason on the webhook endpoint object. Returned everywhere an endpoint is. We now turn off an endpoint whose deliveries have all failed for 72 hours, at least 48 hours after we email a warning. When we do, enabled is false, disabled_at is the time, and disabled_reason is consecutive_failures. Both are null while the endpoint is on, and when you turned it off yourself. See When an endpoint keeps failing.

Changed

  • Dashboard users need permission to manage webhooks to change them. This covers adding, editing, enabling, disabling and deleting endpoints, rotating and viewing signing secrets, and resending events. API keys are unchanged: webhooks:write for changes and webhooks:read for reads, including the signing secret.

Added

  • Virtual accounts. POST /v1/virtual-accounts issues a fixed NGN bank account number for your platform or a connected account, and GET /v1/virtual-accounts reads it back. Each deposit becomes a payment with a collection.succeeded webhook whose checkout_id is null. Creating twice returns the same number rather than a second one. NGN is the only currency today. The account needs an active virtual_accounts capability. Request it through the account API and complete the returned requirements, including the representative’s BVN. The two new permissions, virtual_accounts:read and virtual_accounts:write, are not on keys that already exist. Add them to your key before your first call, or it returns 403. See Virtual accounts.
  • payment_method now reports the rail used for a virtual account deposit. It reads NGN_BANK_TRANSFER, the same corridor name a checkout writes for that rail. We also corrected the card description in the reference: a card payment reports USD_CARD or NGN_CARD, not a bare CARD.
  • payment_method_details on the payment object and collection webhooks. Returned by GET /v1/payments/{payment_id} and sent on collection.succeeded, but not on the payment list. For a bank transfer, it names the sender (sender_name, sender_bank, sender_account_number), the interbank session_id, and the account number that received the money. Use these details to distinguish deposits into the same virtual account. It is null on payments taken before this shipped and on methods that provide no payer details. See the payment object.

Added

  • customer is now optional on checkout session creation. Omit it and the hosted checkout page collects the buyer’s email and name before they can pay. It stays required for a subscription checkout, which has no later opportunity to collect it. See Guest checkout.
  • customer_creation on checkout session creation. Decides whether a buyer who identifies themselves on the hosted page also becomes a customer record. if_required, the default, keeps them out of your directory: customer stays null and no customer.created or customer.updated webhook fires, while the buyer’s email and name reach you on customer_details and their purchases group together in your dashboard. always creates the record, matched by email to an existing customer where you already have that address, and returns it on customer as normal. It is ignored for a subscription or setup checkout, which always create a customer, because recurring billing needs a durable record to keep the renewal card on. See Whether a guest becomes a customer.
  • customer_details on the checkout session object and the checkout.completed webhook. { email, name }, present whenever an identity was collected, whether or not a customer record exists for it. customer is unchanged, so nothing that reads it breaks. What gains an identity today is every checkout that used to send customer: null and nothing else, such as a payment link with a fixed price. See customer vs customer_details.

Changed

  • Two new error codes say why a live checkout can offer no payment method. CHECKOUT_HAS_NO_PAYMENT_METHOD covered three different problems with one message, and named the wrong one for the two most common: an account that is not live yet, and an account whose methods have been restricted. Those now have their own codes, ACCOUNT_NOT_ACTIVATED and ACCOUNT_PAYMENT_METHODS_RESTRICTED, both still 400, and returned everywhere a checkout is created. CHECKOUT_HAS_NO_PAYMENT_METHOD still fires, now only when the account has no payment method configured at all, and CHECKOUT_RESTRICTION_LEAVES_NO_PAYMENT_METHOD is unchanged. If you branch on the old code to detect a not-yet-live account, add the two new codes. Sandbox is unaffected: every method is available there. See Why a checkout has no payment method.

Added

  • payment_method_options on checkout sessions and payment links. Restricts which payment methods a checkout offers, and which currencies each one is offered in. Keys are card, bank_transfer, mobile_money, and crypto. A method you leave out is not offered; a method you include with no currencies is offered in all of them. For crypto the currencies are asset codes such as USDT_TRC20 rather than fiat codes.
    Before this, you could turn a whole payment method on or off but not reach inside it, so “cards, but only in USD” was not expressible: you either took every card currency or dropped cards entirely. Restricting only ever narrows what the customer sees. It cannot add a method or a currency your account is not already enabled for, and if a restriction leaves nothing payable the request is rejected rather than creating a checkout nobody can complete. The restriction is enforced when the payment is priced and confirmed, not only on the page, so a request that names an excluded method or currency is refused with PAYMENT_METHOD_NOT_ALLOWED. See Restrict methods and currencies.

Added

  • Three more dispute evidence fields. access_activity_log (a text log of the customer’s access or activity), plus cancellation_policy_attachment_id and refund_policy_attachment_id for the policy documents themselves. Both policy attachments take a document_id from Upload Dispute Document and pair with the existing cancellation_policy_disclosure and refund_policy_disclosure text: the text states the policy, the file shows the customer was given it. Accepted on PATCH /v1/disputes/{dispute_id}/evidence and returned on Get Dispute.
  • billing_address on the customer object. A nested object with line1, line2, city, state, postal_code, and country (two-letter code), or null when unset. Accepted on POST /v1/customers and PATCH /v1/customers/{customer_id}, and returned everywhere a customer is, including customer.created and customer.updated webhooks and the customer embedded in subscription and invoice payloads.
  • On update, the address is replaced, not merged: omit billing_address to leave it untouched, send null to clear it, or send an object to replace every component (anything you leave out becomes null). See the customer object.
  • Disputes now arrive with customer_email_address, customer_name, and billing_address prefilled from the customer record where we have them. They remain ordinary editable evidence.

Added

  • checkout.completed and checkout.expired webhook events. checkout.completed fires whenever a checkout session finishes, across payment, setup, and subscription modes, and carries a new payment_status field (paid or no_payment_required) so you can tell a real payment apart from a free or setup completion in one event. checkout.expired fires when an open checkout lapses past its expiry without the customer completing it. See checkout.completed and checkout.expired.
  • A free ($0) checkout now settles charge-lessly: no charge is created, and the only event you’ll get is checkout.completed with payment_status: "no_payment_required". Previously this path minted a shell charge; there’s nothing to reconcile against it now.

Removed

  • collection.abandoned is gone. Use checkout.expired instead, it’s the direct replacement and fires from the same expiry sweep.

Breaking

The payment resource moved and its fields were renamed. The old payins paths are gone.On the payment object:
  • charge_id is now payment_id.
  • products is now line_items.
  • organization_id was removed.
  • The response gained billing_reason, subscription_id, and a nested invoice object.
This object is also embedded as charge on the checkout detail response (GET /v1/checkout-sessions/{id}), so checkout.charge.charge_id becomes checkout.charge.payment_id and checkout.charge.products becomes checkout.charge.line_items.Other resources moved out of the /v1/payments namespace so the payment resource owns its root:Subscription line items now report pricing as price_type (fixed, free, custom) instead of amount_type. This applies to the subscription response and the customer.subscription.* webhook payloads.The customer object is now { email, name }. first_name and last_name were removed everywhere: customer responses, the customer.* webhook payloads and every embedded customer object, and the POST/PATCH /v1/customers request bodies. Any code reading customer.first_name breaks; read customer.name instead (split client-side if you need parts).

Added

  • The overlay checkout SDK. Load bachs.js from your checkout origin, or npm install @bachs/js for a typed loader, and open any checkout session in a modal with Bachs.Checkout.open({ checkoutUrl }). Lifecycle events included. See Add an overlay checkout.
  • name is now optional for a new customer on create checkout session; email alone is enough. If provided, it must be non-blank.
  • customer_name and customer_email on invoice responses, so lists can show who was billed without a second lookup.
  • success_url on create checkout session: the primary field for where a customer is redirected after a successful payment. return_url is kept as a deprecated alias.
  • expires_in_minutes on create checkout session (1 to 1440, default 60) to control session lifetime.
  • billing_reason on the payment object: purchase, subscription_create, subscription_cycle, or subscription_update.
  • invoice and subscription_id on the payment object for subscription payments.
  • metadata on the subscription object, on the subscription response and the customer.subscription.* webhook payloads. Set it by attaching metadata to the checkout session that starts the subscription (it propagates on success), or update it later with metadata on PATCH /v1/subscriptions/{id} (a standalone intent). Updates merge: sent keys are added or overwritten, a key sent with an empty-string value is removed, and sending "" clears everything. The system checkout_id key is preserved and cannot be overwritten.
  • search query parameter on list customers to filter by email or name.

Fixed

  • The payment object now returns fee_usd (previously documented as fee, which was never returned). The incorrect settlement_currency and settlement_amount fields were removed from the object; they were never returned.
  • Customer IDs are documented with the correct cust_ prefix (previously shown as cus_).
  • Checkout examples in the quickstart used a products field and first_name/last_name on the customer; the real fields are product_cart and name, and the examples now say so.

Breaking

Webhook management moved to /v1/webhooks and is now API-key accessible. The endpoint-management routes moved off the /v1/developer/webhooks path.Any code calling the old /v1/developer/webhooks/* paths breaks; update the base path to /v1/webhooks/*. Authentication is unchanged for dashboard sessions, and API keys with the webhooks:read / webhooks:write scopes now work too.

Added

  • Webhook Endpoint API. You can now create, list, update, and delete webhook endpoints, rotate signing secrets, read delivery metrics, and list or resend events, all with an API key. Thirteen endpoints under /v1/webhooks. See the webhook endpoint object.

Fixed

  • The webhooks documentation was scattered across three places. It now lives in one Webhooks section: Concepts, Events, and the Endpoint API.