> ## Documentation Index
> Fetch the complete documentation index at: https://docs.straddle.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Charges: collect ACH bank payments

> Use Straddle charges to collect ACH payments from customer bank accounts, with built-in balance checks, fraud screening, and configurable processing rails.

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.

<Info>
  Charges are [ACH debit transactions](/help/ACH101/ach-debit) that require proper [customer authorization](/help/Payment-Compliance/ach-auth). Understanding [ACH basics](/help/ACH101/ach-basics) helps ensure compliant implementation and reduces return rates.
</Info>

## What you'll learn

This comprehensive guide walks 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.

<Note>
  Charges require a `paykey`: a token representing a verified bank account. Learn about connecting bank accounts in the [Bridge guide](/guides/bridge/overview).
</Note>

## The charge object

A charge represents a debit transaction with comprehensive status tracking and metadata:

```json theme={null}
{
  "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": "john@example.com"
  },
  "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 |
| - | - | - |
| `id` | string | Unique charge identifier |
| `paykey` | string | Token for customer's bank account |
| `amount` | integer | Amount in cents (10000 = \$100.00) |
| `currency` | string | Currency code (USD only) |
| `description` | string | Statement descriptor |
| `payment_date` | string | Processing date (YYYY-MM-DD) |
| `status` | string | Current payment status |
| `status_details` | object | Detailed status information |
| `external_id` | string | Your unique reference |
| `consent_type` | string | How consent was obtained |
| `trace_number` | string | ACH network tracking number |
| `funding_ids` | array | Related settlement events |

## Creating a charge

View detailed schema and request information on the [Create a Charge API Reference](/api-reference/charges/create).

<RequestExample>
  ```bash cURL theme={null}
  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"
      }
    }'
  ```

  ```javascript Node.js theme={null}
  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"
    }
  });
  ```

  ```python Python theme={null}
  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"
      }
  )
  ```
</RequestExample>

<ResponseExample>
  ```json 201 Created theme={null}
  {
    "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"
    }
  }
  ```

  ```json 400 Bad Request theme={null}
  {
    "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."
        }
      ]
    }
  }
  ```
</ResponseExample>

## Required parameters

<ParamField body="paykey" type="string" required>
  Payment key for the customer's verified bank account. Obtained via Bridge flow.
</ParamField>

<ParamField body="amount" type="integer" required>
  Amount to charge in cents. Must be positive. Example: `10000` for \$100.00
</ParamField>

<ParamField body="currency" type="string" required>
  ISO 4217 currency code. Currently only `"USD"` is supported.
</ParamField>

<ParamField body="description" type="string" required>
  Description appearing on bank statements. Maximum 80 characters. Be clear and recognizable.
</ParamField>

<ParamField body="payment_date" type="string" required>
  Date to process payment (YYYY-MM-DD). Can be today or future-dated up to 90 days.
</ParamField>

<ParamField body="consent_type" type="string" required>
  How customer consent was obtained:

  * `"internet"` - Online or mobile app authorization
  * `"signed"` - Signed agreement or contract
  * `"telephone"` - Phone authorization

  <Note>
    Learn more about [authorization requirements](/help/Payment-Compliance/ach-auth) and the difference between [internet vs. contract authorization](/help/Payment-Compliance/consent-types) for ACH compliance.
  </Note>
</ParamField>

<ParamField body="device" type="object" required>
  Customer device information for fraud prevention.

  <Expandable title="Device object">
    <ParamField body="ip_address" type="string" required>
      Customer's IP address at authorization time. Use `"0.0.0.0"` for offline consent.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="external_id" type="string" required>
  Your unique identifier for this charge. Must be unique across all charges. Used for idempotency.
</ParamField>

<ParamField body="config" type="object" required>
  Processing configuration options.

  <Expandable title="Config object">
    <ParamField body="balance_check" type="string" required>
      Balance verification mode:

      * `"enabled"` - Attempts check, proceeds if unavailable (recommended)
      * `"required"` - Must verify balance to proceed
      * `"disabled"` - No verification attempted
    </ParamField>

    <ParamField body="sandbox_outcome" type="string">
      For testing only. Simulates specific outcomes in sandbox environment.
    </ParamField>
  </Expandable>
</ParamField>

## Optional parameters

<ParamField body="metadata" type="object">
  Custom key-value pairs. Maximum 20 keys, 40 characters per key/value.
</ParamField>

## Payment status lifecycle

Charges progress through these statuses:

| Status | Description | Can Modify? |
| - | - | - |
| `created` | Initial state, awaiting verification | ✅ Yes |
| `scheduled` | Verified and queued for processing | ✅ Yes |
| `pending` | Sent to payment network | ❌ No |
| `paid` | Successfully completed | ❌ No |
| `failed` | Declined before funding | ❌ Terminal |
| `reversed` | Returned after funding | ❌ Terminal |
| `cancelled` | Stopped before network submission | ❌ Terminal |
| `on_hold` | Paused for review | ⚠️ Depends |

<Warning>
  **Critical**: Once a charge reaches `pending` status, it has been submitted to the payment network and CANNOT be stopped, held, or cancelled.
</Warning>

### Status details

Every status change includes detailed information:

```json theme={null}
"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 |
| - | - |
| `message` | Human-readable explanation |
| `reason` | Machine-readable reason code |
| `source` | Where the status change originated |
| `code` | ACH return code (when applicable) |
| `changed_at` | When the status changed |

For complete status reasons and ACH codes, see [Payment Statuses Guide](/guides/payments/statuses). Learn about [ACH return codes](/help/nacha-rules/ach-return) and [NACHA compliance](/help/nacha-rules/ach-rules) for better error handling.

## Common operations

### [Retrieve a charge](/api-reference/charges/retrieve)

```bash theme={null}
curl -X GET https://sandbox.straddle.com/v1/charges/{charge_id} \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### [Update a charge](/api-reference/charges/update)

Update charges in `created` or `scheduled` status:

```bash theme={null}
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 a charge](/api-reference/charges/cancel)

Cancel charges before they reach `pending` status:

```bash theme={null}
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"
  }'
```

### [Place on hold](/api-reference/charges/hold)

Pause a charge for review (only in `created` or `scheduled` status):

```bash theme={null}
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 a hold](/api-reference/charges/release)

Release user-initiated holds only:

```bash theme={null}
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"
  }'
```

<Note>
  System holds (`watchtower` source) cannot be released via API. They require manual review in the dashboard.
</Note>

### Resubmit a charge

[Re-attempt](/guides/payments/statuses#resubmit-failed-payments) a charge using the [resubmit](https://straddle.dev/api-reference#tag/charge/POST/v1/charges/\{id}/resubmit) endpoint. This creates a new transaction that inherits the original payment's details.

```shellscript theme={null}
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 |
| - | - | - |
| `enabled` | Attempts check, proceeds if unavailable | **Recommended** - Best balance |
| `required` | Must verify balance to proceed | High-value transactions |
| `disabled` | No verification attempted | Future-dated charges |

<Tip>
  Balance verification is part of Straddle's [account validation](/help/Payment-Compliance/account-validation) process that helps reduce NSF returns and improve payment success rates.
</Tip>

<Warning>
  **`required`** fails charges if:

  * The paykey was created with manual bank account entry
  * The bank doesn't provide balance information
  * Balance service is temporarily unavailable
</Warning>

## Handling failures

When charges fail, check `status_details` for specific information:

```javascript theme={null}
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:

```json theme={null}
{
  "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](/webhooks/overview/101) to receive real-time charge updates.

## Best practices

<CardGroup cols={2}>
  <Card icon="fingerprint" title="Use Idempotency">
    Always include a unique `external_id` to prevent duplicate charges
  </Card>

  <Card icon="bell" title="Monitor Status Changes">
    Set up webhooks to track payment lifecycle in real-time
  </Card>

  <Card icon="shield" title="Handle Failures Gracefully">
    Implement retry logic for transient failures like NSF
  </Card>

  <Card icon="flask" title="Test Thoroughly">
    Use sandbox to test all failure scenarios before production
  </Card>
</CardGroup>

## Next steps

<CardGroup cols={2}>
  <Card icon="code" href="/api-reference/charges/create" title="API Reference">
    Complete charge endpoint documentation
  </Card>

  <Card icon="list-check" href="/guides/payments/statuses" title="Payment Statuses">
    Detailed status flows and ACH codes
  </Card>

  <Card icon="arrow-up-from-line" href="/guides/payments/payouts" title="Payouts Guide">
    Learn about sending money to customers
  </Card>

  <Card icon="webhook" href="/webhooks/overview" title="Webhooks">
    Set up real-time notifications
  </Card>
</CardGroup>


## Related topics

- [Create a charge](/api-reference/charges/create.md)
- [Payments overview](/guides/payments/overview.md)
- [Update a charge](/api-reference/charges/update.md)
- [Resubmit a charge](/api-reference/charges/resubmit.md)
- [Lookup a charge](/api-reference/charges/retrieve.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.