Charges are debit transactions that pull funds from a customer’s bank account through the ACH network. This guide covers the complete charge lifecycle—from creation through settlement—including status management, balance verification, and handling returns.
What You’ll Learn
This comprehensive guide will walk you through:
Creating charges with required fields and optional configurations
Understanding the payment status lifecycle and when charges can be modified
Implementing balance verification to reduce NSF failures
Handling failures and returns with specific reason codes
Managing charges through hold, release, and cancellation operations
Testing charge scenarios in sandbox before production
Whether you’re implementing subscription billing, one-time purchases, invoice collection, or account funding, this guide provides the patterns and best practices for successful charge implementation.
Charges require a paykey - a token representing a verified bank account. Learn about connecting bank accounts in the Bridge guide .
The Charge Object
A charge represents a debit transaction with comprehensive status tracking and metadata:
{
"id" : "ch_def456abc" ,
"paykey" : "pk_abc123xyz" ,
"amount" : 10000 ,
"currency" : "USD" ,
"description" : "Monthly subscription - October 2024" ,
"payment_date" : "2024-10-01" ,
"status" : "paid" ,
"status_details" : {
"message" : "Payment successfully completed" ,
"reason" : "ok" ,
"source" : "system" ,
"code" : null ,
"changed_at" : "2024-10-03T09:00:00Z"
},
"external_id" : "inv_12345" ,
"consent_type" : "internet" ,
"device" : {
"ip_address" : "192.168.1.1"
},
"config" : {
"balance_check" : "enabled"
},
"metadata" : {
"order_id" : "ord_789" ,
"customer_ref" : "cust_456"
},
"payment_rail" : "ACH" ,
"paykey_details" : {
"bank_name" : "Chase Bank" ,
"last4" : "6789" ,
"account_type" : "checking"
},
"customer_details" : {
"name" : "John Doe" ,
"email" : "[email protected] "
},
"trace_number" : "071000301234567" ,
"funding_ids" : [ "fe_abc123" ],
"status_history" : [ ... ],
"created_at" : "2024-10-01T10:00:00Z" ,
"updated_at" : "2024-10-03T09:00:00Z"
}
Core Fields
Field Type Description idstring Unique charge identifier paykeystring Token for customer’s bank account amountinteger Amount in cents (10000 = $100.00) currencystring Currency code (USD only) descriptionstring Statement descriptor payment_datestring Processing date (YYYY-MM-DD) statusstring Current payment status status_detailsobject Detailed status information external_idstring Your unique reference consent_typestring How consent was obtained trace_numberstring ACH network tracking number funding_idsarray Related settlement events
Creating a Charge
View detailed schema and request information on the Create a Charge API Reference
curl -X POST https://sandbox.straddle.com/v1/charges \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"paykey": "pk_abc123xyz",
"amount": 10000,
"currency": "USD",
"description": "Monthly subscription - October 2024",
"payment_date": "2024-10-01",
"consent_type": "internet",
"device": {
"ip_address": "192.168.1.1"
},
"external_id": "inv_12345",
"config": {
"balance_check": "enabled"
}
}'
const charge = await straddle . charges . create ({
paykey: "pk_abc123xyz" ,
amount: 10000 ,
currency: "USD" ,
description: "Monthly subscription - October 2024" ,
payment_date: "2024-10-01" ,
consent_type: "internet" ,
device: {
ip_address: "192.168.1.1"
},
external_id: "inv_12345" ,
config: {
balance_check: "enabled"
}
});
charge = straddle.charges.create(
paykey = "pk_abc123xyz" ,
amount = 10000 ,
currency = "USD" ,
description = "Monthly subscription - October 2024" ,
payment_date = "2024-10-01" ,
consent_type = "internet" ,
device = {
"ip_address" : "192.168.1.1"
},
external_id = "inv_12345" ,
config = {
"balance_check" : "enabled"
}
)
201 Created
400 Bad Request
{
"meta" : {
"api_request_id" : "3a2b1c4d-0e6f-4a88-9876-123456abcdef" ,
"api_request_timestamp" : "2024-10-01T10:00:00Z"
},
"response_type" : "object" ,
"data" : {
"id" : "ch_def456abc" ,
"paykey" : "pk_abc123xyz" ,
"amount" : 10000 ,
"currency" : "USD" ,
"description" : "Monthly subscription - October 2024" ,
"payment_date" : "2024-10-01" ,
"status" : "created" ,
"status_details" : {
"message" : "Payment successfully created and awaiting validation." ,
"reason" : "ok" ,
"source" : "system" ,
"changed_at" : "2024-10-01T10:00:00Z"
},
"external_id" : "inv_12345" ,
"consent_type" : "internet" ,
"device" : {
"ip_address" : "192.168.1.1"
},
"config" : {
"balance_check" : "enabled"
},
"metadata" : {},
"created_at" : "2024-10-01T10:00:00Z" ,
"updated_at" : "2024-10-01T10:00:00Z"
}
}
{
"meta" : {
"api_request_id" : "f1b1b1b1-1b1b-1b1b-1b1b-1b1b1b1b1b1b"
},
"response_type" : "error" ,
"error" : {
"status" : 400 ,
"type" : "validation_error" ,
"title" : "Invalid Input Data" ,
"detail" : "The request contains invalid field values." ,
"items" : [
{
"reference" : "paykey" ,
"detail" : "The provided paykey is invalid or expired."
}
]
}
}
Required Parameters
Payment key for the customer’s verified bank account. Obtained via Bridge flow.
Amount to charge in cents. Must be positive. Example: 10000 for $100.00
ISO 4217 currency code. Currently only "USD" is supported.
Description appearing on bank statements. Maximum 80 characters. Be clear and recognizable.
Date to process payment (YYYY-MM-DD). Can be today or future-dated up to 90 days.
How customer consent was obtained:
"internet" - Online or mobile app authorization
"signed" - Signed agreement or contract
"telephone" - Phone authorization
Customer device information for fraud prevention. Customer’s IP address at authorization time. Use "0.0.0.0" for offline consent.
Your unique identifier for this charge. Must be unique across all charges. Used for idempotency.
Processing configuration options. Balance verification mode:
"enabled" - Attempts check, proceeds if unavailable (recommended)
"required" - Must verify balance to proceed
"disabled" - No verification attempted
For testing only. Simulates specific outcomes in sandbox environment.
Optional Parameters
Custom key-value pairs. Maximum 20 keys, 40 characters per key/value.
Payment Status Lifecycle
Charges progress through these statuses:
Status Description Can Modify? createdInitial state, awaiting verification ✅ Yes scheduledVerified and queued for processing ✅ Yes pendingSent to payment network ❌ No paidSuccessfully completed ❌ No failedDeclined before funding ❌ Terminal reversedReturned after funding ❌ Terminal cancelledStopped before network submission ❌ Terminal on_holdPaused for review ⚠️ Depends
Critical : Once a charge reaches pending status, it has been submitted to the payment network and CANNOT be stopped, held, or cancelled.
Status Details
Every status change includes detailed information:
"status_details" : {
"message" : "Payment failed due to insufficient funds" ,
"reason" : "insufficient_funds" ,
"source" : "bank_decline" ,
"code" : "R01" ,
"changed_at" : "2024-10-02T14:30:00Z"
}
Field Description messageHuman-readable explanation reasonMachine-readable reason code sourceWhere the status change originated codeACH return code (when applicable) changed_atWhen the status changed
For complete status reasons and ACH codes, see Payment Statuses Guide . Learn about ACH return codes and NACHA compliance for better error handling.
Common Operations
curl -X GET https://sandbox.straddle.com/v1/charges/{charge_id} \
-H "Authorization: Bearer YOUR_API_KEY"
Update charges in created or scheduled status:
curl -X PUT https://sandbox.straddle.com/v1/charges/{charge_id} \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"description": "Updated amount - Monthly subscription",
...
}'
Cancel charges before they reach pending status:
curl -X POST https://sandbox.straddle.com/v1/charges/{charge_id}/cancel \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Customer requested cancellation"
}'
Pause a charge for review (only in created or scheduled status):
curl -X POST https://sandbox.straddle.com/v1/charges/{charge_id}/hold \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Awaiting inventory confirmation"
}'
Release user-initiated holds only:
curl -X POST https://sandbox.straddle.com/v1/charges/{charge_id}/release \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "Inventory confirmed"
}'
System holds (watchtower source) cannot be released via API. They require manual review in the dashboard.
Resubmit a Charge
Re-attempt a charge using the resubmit endpoint. This creates a new transaction that inherits the original payment’s details.
curl -X POST https://api.straddle.com/v1/charges/{id}/resubmit \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"payment_date": "2026-03-01",
"description": "Customer requested retry after payday",
"external_id": "my-custom-external-id"
}'
Balance Verification
Balance checks help reduce NSF returns:
Mode Behavior Use Case enabledAttempts check, proceeds if unavailable Recommended - Best balancerequiredMust verify balance to proceed High-value transactions disabledNo verification attempted Future-dated charges
Balance verification is part of Straddle’s account validation process that helps reduce NSF returns and improve payment success rates.
required will fail charges if:
The paykey was created with manual bank account entry
The bank doesn’t provide balance information
Balance service is temporarily unavailable
Handling Failures
When charges fail, check status_details for specific information:
if ( charge . status === 'failed' ) {
switch ( charge . status_details . reason ) {
case 'insufficient_funds' :
// Retry after notifying customer
break ;
case 'closed_bank_account' :
// Request new payment method
break ;
case 'disputed' :
// Do not retry - investigate
break ;
case 'risk_review' :
// Compliance review needed
break ;
}
}
Testing in Sandbox
Use sandbox_outcome in the config to simulate scenarios:
{
"paykey" : "pk_test123" ,
"amount" : 5000 ,
"config" : {
"balance_check" : "enabled" ,
"sandbox_outcome" : "paid" // or "failed_insufficient_funds", etc.
}
// ... other required fields
}
Common test scenarios:
"paid" - Successful charge
"failed_insufficient_funds" - NSF failure
"on_hold_daily_limit" - Risk hold
"reversed_customer_dispute" - Post-funding reversal
Webhooks
Set up webhooks to receive real-time charge updates
Best Practices
Use Idempotency Always include a unique external_id to prevent duplicate charges
Monitor Status Changes Set up webhooks to track payment lifecycle in real-time
Handle Failures Gracefully Implement retry logic for transient failures like NSF
Test Thoroughly Use sandbox to test all failure scenarios before production
Next Steps
API Reference Complete charge endpoint documentation
Payment Statuses Detailed status flows and ACH codes
Payouts Guide Learn about sending money to customers
Webhooks Set up real-time notifications