> ## 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 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.

<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`** 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
</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>
