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
payoutscapability, destination approval and sufficient available funds still apply. These withdrawals are to the account holder’s own destinations. Register destinations withPOST /v1/payouts/destinationsand checkis_usablebefore requesting a payout. Set the destinationtypeexplicitly for bank or mobile money destinations, and choose aschemefor 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 itsquote_idwith the destination ID, withoutamount, when creating the payout. USD-to-USD payouts useamountwithout a conversion quote. A successful create response confirms acceptance, not delivery; track the payout untilcompletedorfailed. For connected accounts, useX-Account-Idwith that account’s own balance and approved destination. Add scheme-based international bank destinations through the destination API rather than the inline onboardingpayout_destinationfield. See Global Payouts and International bank payouts. -
enabledonPATCH /v1/webhooks/endpoints/{endpoint_id}. Sendfalseto stop all deliveries to an endpoint, ortrueto 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_atanddisabled_reasonon 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,enabledisfalse,disabled_atis the time, anddisabled_reasonisconsecutive_failures. Both arenullwhile 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:writefor changes andwebhooks:readfor reads, including the signing secret.
Added
-
Virtual accounts.
POST /v1/virtual-accountsissues a fixed NGN bank account number for your platform or a connected account, andGET /v1/virtual-accountsreads it back. Each deposit becomes a payment with acollection.succeededwebhook whosecheckout_idisnull. Creating twice returns the same number rather than a second one. NGN is the only currency today. The account needs an activevirtual_accountscapability. Request it through the account API and complete the returned requirements, including the representative’s BVN. The two new permissions,virtual_accounts:readandvirtual_accounts:write, are not on keys that already exist. Add them to your key before your first call, or it returns403. See Virtual accounts. -
payment_methodnow reports the rail used for a virtual account deposit. It readsNGN_BANK_TRANSFER, the same corridor name a checkout writes for that rail. We also corrected the card description in the reference: a card payment reportsUSD_CARDorNGN_CARD, not a bareCARD. -
payment_method_detailson the payment object and collection webhooks. Returned byGET /v1/payments/{payment_id}and sent oncollection.succeeded, but not on the payment list. For a bank transfer, it names the sender (sender_name,sender_bank,sender_account_number), the interbanksession_id, and the account number that received the money. Use these details to distinguish deposits into the same virtual account. It isnullon payments taken before this shipped and on methods that provide no payer details. See the payment object.
Added
-
customeris 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_creationon 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:customerstaysnulland nocustomer.createdorcustomer.updatedwebhook fires, while the buyer’s email and name reach you oncustomer_detailsand their purchases group together in your dashboard.alwayscreates the record, matched by email to an existing customer where you already have that address, and returns it oncustomeras normal. It is ignored for a subscription orsetupcheckout, 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_detailson the checkout session object and thecheckout.completedwebhook.{ email, name }, present whenever an identity was collected, whether or not a customer record exists for it.customeris unchanged, so nothing that reads it breaks. What gains an identity today is every checkout that used to sendcustomer: nulland nothing else, such as a payment link with a fixed price. Seecustomervscustomer_details.
Changed
-
Two new error codes say why a live checkout can offer no payment method.
CHECKOUT_HAS_NO_PAYMENT_METHODcovered 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_ACTIVATEDandACCOUNT_PAYMENT_METHODS_RESTRICTED, both still400, and returned everywhere a checkout is created.CHECKOUT_HAS_NO_PAYMENT_METHODstill fires, now only when the account has no payment method configured at all, andCHECKOUT_RESTRICTION_LEAVES_NO_PAYMENT_METHODis 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_optionson checkout sessions and payment links. Restricts which payment methods a checkout offers, and which currencies each one is offered in. Keys arecard,bank_transfer,mobile_money, andcrypto. A method you leave out is not offered; a method you include with nocurrenciesis offered in all of them. Forcryptothe currencies are asset codes such asUSDT_TRC20rather 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 withPAYMENT_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), pluscancellation_policy_attachment_idandrefund_policy_attachment_idfor the policy documents themselves. Both policy attachments take adocument_idfrom Upload Dispute Document and pair with the existingcancellation_policy_disclosureandrefund_policy_disclosuretext: the text states the policy, the file shows the customer was given it. Accepted onPATCH /v1/disputes/{dispute_id}/evidenceand returned on Get Dispute. -
billing_addresson the customer object. A nested object withline1,line2,city,state,postal_code, andcountry(two-letter code), ornullwhen unset. Accepted onPOST /v1/customersandPATCH /v1/customers/{customer_id}, and returned everywhere a customer is, includingcustomer.createdandcustomer.updatedwebhooks and the customer embedded in subscription and invoice payloads. -
On update, the address is replaced, not merged: omit
billing_addressto leave it untouched, sendnullto clear it, or send an object to replace every component (anything you leave out becomesnull). See the customer object. -
Disputes now arrive with
customer_email_address,customer_name, andbilling_addressprefilled from the customer record where we have them. They remain ordinary editable evidence.
Added
checkout.completedandcheckout.expiredwebhook events.checkout.completedfires whenever a checkout session finishes, acrosspayment,setup, andsubscriptionmodes, and carries a newpayment_statusfield (paidorno_payment_required) so you can tell a real payment apart from a free or setup completion in one event.checkout.expiredfires when an open checkout lapses past its expiry without the customer completing it. Seecheckout.completedandcheckout.expired.- A free (
$0) checkout now settles charge-lessly: no charge is created, and the only event you’ll get ischeckout.completedwithpayment_status: "no_payment_required". Previously this path minted a shell charge; there’s nothing to reconcile against it now.
Removed
collection.abandonedis gone. Usecheckout.expiredinstead, it’s the direct replacement and fires from the same expiry sweep.
Breaking
The payment resource moved and its fields were renamed. The oldpayins paths are gone.On the payment object:
charge_idis nowpayment_id.productsis nowline_items.organization_idwas removed.- The response gained
billing_reason,subscription_id, and a nestedinvoiceobject.
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.jsfrom your checkout origin, ornpm install @bachs/jsfor a typed loader, and open any checkout session in a modal withBachs.Checkout.open({ checkoutUrl }). Lifecycle events included. See Add an overlay checkout. -
nameis now optional for a new customer on create checkout session;emailalone is enough. If provided, it must be non-blank. -
customer_nameandcustomer_emailon invoice responses, so lists can show who was billed without a second lookup. -
success_urlon create checkout session: the primary field for where a customer is redirected after a successful payment.return_urlis kept as a deprecated alias. -
expires_in_minuteson create checkout session (1 to 1440, default 60) to control session lifetime. -
billing_reasonon the payment object:purchase,subscription_create,subscription_cycle, orsubscription_update. -
invoiceandsubscription_idon the payment object for subscription payments. -
metadataon the subscription object, on the subscription response and thecustomer.subscription.*webhook payloads. Set it by attaching metadata to the checkout session that starts the subscription (it propagates on success), or update it later withmetadataonPATCH /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 systemcheckout_idkey is preserved and cannot be overwritten. -
searchquery parameter on list customers to filter by email or name.
Fixed
- The payment object now returns
fee_usd(previously documented asfee, which was never returned). The incorrectsettlement_currencyandsettlement_amountfields were removed from the object; they were never returned. - Customer IDs are documented with the correct
cust_prefix (previously shown ascus_). - Checkout examples in the quickstart used a
productsfield andfirst_name/last_nameon the customer; the real fields areproduct_cartandname, 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.

