curl --request POST \
--url https://api.bachs.io/v1/charges \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customer": "cust_1a2b3c4d5e6f",
"amount": "29.00",
"currency": "USD",
"description": "April usage",
"reference": "INV-2026-04-881"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
customer: 'cust_1a2b3c4d5e6f',
amount: '29.00',
currency: 'USD',
description: 'April usage',
reference: 'INV-2026-04-881'
})
};
fetch('https://api.bachs.io/v1/charges', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.bachs.io/v1/charges"
payload = {
"customer": "cust_1a2b3c4d5e6f",
"amount": "29.00",
"currency": "USD",
"description": "April usage",
"reference": "INV-2026-04-881"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.bachs.io/v1/charges"
payload := strings.NewReader("{\n \"customer\": \"cust_1a2b3c4d5e6f\",\n \"amount\": \"29.00\",\n \"currency\": \"USD\",\n \"description\": \"April usage\",\n \"reference\": \"INV-2026-04-881\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"payment_id": "ch_389305e973a841cc",
"status": "processing",
"amount": "30.85",
"amount_paid": "0.00",
"amount_remaining": "30.85",
"currency": "USD",
"fees": {
"amount": "1.85",
"currency": "USD"
},
"payment_method": "CARD",
"checkout_id": null,
"narration": "April usage",
"reference": "INV-2026-04-881",
"billing_reason": "purchase",
"customer": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"created_at": "2026-04-27T12:05:00.000Z",
"updated_at": "2026-04-27T12:05:00.000Z"
}{
"detail": "Invalid request parameters",
"error_code": "VALIDATION_ERROR",
"errors": [
{
"field": "amount",
"message": "Amount must be a positive decimal string",
"type": "value_error"
}
]
}{
"detail": "Invalid API key",
"error_code": "UNAUTHORIZED"
}{
"detail": "API key does not have permission for this operation",
"error_code": "FORBIDDEN"
}{
"detail": "Resource not found",
"error_code": "NOT_FOUND"
}{
"detail": "Rate limit exceeded. Please retry after a few seconds.",
"error_code": "TOO_MANY_REQUESTS"
}{
"detail": "An unexpected error occurred. Please try again later.",
"error_code": "INTERNAL_SERVER_ERROR"
}Create a charge
In beta. This endpoint might change, including field names and the shape of the response. Pin your integration to what you test.
Charge a customer’s saved card off-session, meaning with nobody on a payment page. Use this for money a customer agreed to once and you collect later, such as a usage invoice or a top-up. An off-session charge cannot ask the customer to authenticate, so a card whose issuer demands it is refused.
The card must have been saved on an earlier checkout. See Charge a saved card.
This always answers with a payment, never an error, when the card is refused: a refusal is an outcome you read from status. The payment is usually processing, and the result reaches you as a collection.succeeded or collection.failed webhook. A card refused while the request is still open comes back already failed. A 201 is not payment received.
Send an Idempotency-Key header. Without one, a retry after a timeout charges the customer twice.
curl --request POST \
--url https://api.bachs.io/v1/charges \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customer": "cust_1a2b3c4d5e6f",
"amount": "29.00",
"currency": "USD",
"description": "April usage",
"reference": "INV-2026-04-881"
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
customer: 'cust_1a2b3c4d5e6f',
amount: '29.00',
currency: 'USD',
description: 'April usage',
reference: 'INV-2026-04-881'
})
};
fetch('https://api.bachs.io/v1/charges', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://api.bachs.io/v1/charges"
payload = {
"customer": "cust_1a2b3c4d5e6f",
"amount": "29.00",
"currency": "USD",
"description": "April usage",
"reference": "INV-2026-04-881"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.bachs.io/v1/charges"
payload := strings.NewReader("{\n \"customer\": \"cust_1a2b3c4d5e6f\",\n \"amount\": \"29.00\",\n \"currency\": \"USD\",\n \"description\": \"April usage\",\n \"reference\": \"INV-2026-04-881\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}{
"payment_id": "ch_389305e973a841cc",
"status": "processing",
"amount": "30.85",
"amount_paid": "0.00",
"amount_remaining": "30.85",
"currency": "USD",
"fees": {
"amount": "1.85",
"currency": "USD"
},
"payment_method": "CARD",
"checkout_id": null,
"narration": "April usage",
"reference": "INV-2026-04-881",
"billing_reason": "purchase",
"customer": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"created_at": "2026-04-27T12:05:00.000Z",
"updated_at": "2026-04-27T12:05:00.000Z"
}{
"detail": "Invalid request parameters",
"error_code": "VALIDATION_ERROR",
"errors": [
{
"field": "amount",
"message": "Amount must be a positive decimal string",
"type": "value_error"
}
]
}{
"detail": "Invalid API key",
"error_code": "UNAUTHORIZED"
}{
"detail": "API key does not have permission for this operation",
"error_code": "FORBIDDEN"
}{
"detail": "Resource not found",
"error_code": "NOT_FOUND"
}{
"detail": "Rate limit exceeded. Please retry after a few seconds.",
"error_code": "TOO_MANY_REQUESTS"
}{
"detail": "An unexpected error occurred. Please try again later.",
"error_code": "INTERNAL_SERVER_ERROR"
}Authorizations
Bearer token authentication. Pass your API key as Authorization: Bearer sk_.... See Authentication for keys, scopes, and sandbox vs production.
Body
The customer to charge, by their cust_ id. They must already have a saved card, or the request is refused with NO_SAVED_PAYMENT_METHOD.
The amount to collect, as a decimal string (for example "29.00"). Must be greater than zero. This is the amount before the processing fee: when your account passes the fee to the customer, the card is charged more than this and the response amount shows the total.
The saved card to charge, by its pm_ id. Omit to charge the customer's default saved card, which is the first card they saved. A card belonging to a different customer is refused with SAVED_PAYMENT_METHOD_NOT_FOUND.
What the charge is for. Returned as narration on the payment, and shown on your dashboard.
255Your own identifier for this charge, returned unchanged on the payment so you can match it to your records.
128Key-value pairs you attach to the charge and get back on the payment and its webhooks.
Response
Success - Charge created and submitted to the card
Detailed payment response for API integrations.
Unique identifier for the payment.
"pay_1a2b3c4d5e"
Current status of the payment. created: the charge exists and no attempt has succeeded yet. processing: an attempt is in flight and is being verified. succeeded: the payment is confirmed and settled in full. accepted: an underpayment or overpayment was accepted as final settlement. failed: the payment failed and no funds were captured. expired: the payment window elapsed before any payment arrived. cancelled: cancelled before completion. refunded: the full amount was returned to the customer. partially_refunded: part of the amount was returned to the customer. auto_refunded: we automatically returned the full amount to the customer. underpaid: the customer paid less than the amount owed. overpaid: the customer paid more than the amount owed.
created, processing, succeeded, accepted, failed, expired, cancelled, refunded, partially_refunded, auto_refunded, underpaid, overpaid "succeeded"
Requested amount in currency.
"10.00"
Payment currency code.
"USD"
Creation timestamp.
"2026-04-27T12:00:00Z"
Last update timestamp.
"2026-04-27T12:00:05Z"
Checkout reference when available.
"order_9876"
Why this payment exists. purchase: a one-time purchase. subscription_create: the first cycle of a new subscription. subscription_cycle: a subscription renewal. subscription_update: an off-cycle charge from a mid-cycle plan change (proration).
purchase, subscription_create, subscription_cycle, subscription_update "purchase"
Checkout identifier, when linked.
"chk_8T9u0V1w2X3y4Z5a"
Whether this payment can currently be refunded.
true
Amount received so far.
"10.00"
Remaining amount still expected.
"0.00"
Processing fee for this payment, converted to USD and expressed as a decimal string. null until the payment settles.
"0.59"
The processing fee on this payment, in the currency it was charged in. Prefer this over fee_usd when the payment was not collected in USD: fee_usd is a conversion of the same fee, this is the amount actually taken. null on a payment that carries no processing fee. See Fees.
Show child attributes
Show child attributes
Whether merchant bears processing cost.
true
What the platform took out of this sale, beside the gross it was taken from, in the base currency of the sale. null when the charge carried no platform fee, and on a charge that split the sale with transfer_data.amount instead. See Platform fees.
null
The seller's contracted share of this sale, in the base currency of the sale. Null on a charge that carries no split, and on one that split the sale with platform_fee instead.
null
Who Bachs's processing fee actually came from on this charge, read back from the ledger posting rather than a flag decided in advance. merchant: the fee came out of the charge. platform: the platform's own balance covered it. On a destination charge this never reads platform; the fee always comes from the charge there. See Processing fees.
merchant, platform "merchant"
Payment method used for this payment. For every method except card, this is the exact corridor collected, such as NGN_BANK_TRANSFER, MOMO_GHS, or CRYPTO. Card charges report CARD rather than USD_CARD or NGN_CARD; read the currency to tell which card corridor collected it.
"NGN_BANK_TRANSFER"
Origin channel (for example api).
"checkout"
payment description/narration.
"Pro plan"
Public metadata stored for this payment.
{ "order_id": "ORD-9876" }
Human-readable payment message derived from status.
"Successful"
Customer information when available.
Show child attributes
Show child attributes
The line items this payment covers.
Show child attributes
Show child attributes
The subscription this payment belongs to, or null for a one-time purchase.
null
The invoice this payment collected. Present only for subscription payments; null for one-time purchases.
Show child attributes
Show child attributes
IDs of any refunds issued for this payment. null if no refund has been created.
["ref_1a2b3c4d5e"]
Chronological list of status changes for this payment.
Show child attributes
Show child attributes
Completion timestamp when available.
"2026-04-27T12:00:05Z"

