ACH Transfers
Tokenize ACH customers, preview transfer cost, and create Dwolla-backed ACH transfers through Payflow.
ACH transfers are server-side flows. They require secret API keys, ACH routing, Dwolla configuration, and valid wallet/provider readiness for the target environment.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /client/v1/ach/customer-tokens | Create an encrypted customer token. |
POST | /client/v1/ach/transfers/preview | Preview routing, fees, and total debit. |
POST | /client/v1/ach/transfers | Create an ACH transfer. |
All three endpoints require a secret key.
The examples assume:
export PAYFLOW_CLIENT_API_URL="https://api.ftosllc.co/client/v1"
export PAYFLOW_SECRET_KEY="sk_test_your_key_id_your_secret"Create a customer token
Customer tokens let Payflow bind customer identity details to an encrypted token before the transfer call.
curl -X POST "$PAYFLOW_CLIENT_API_URL/ach/customer-tokens" \
-H "Authorization: Bearer $PAYFLOW_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"externalCustomerId": "cus_1001",
"email": "[email protected]",
"firstName": "Jane",
"lastName": "Stone",
"phone": "+15555550100",
"metadata": {
"accountTier": "business"
}
}'Response:
{
"success": true,
"data": {
"customerToken": "eyJ...",
"tokenType": "JWE",
"expiresAt": null
},
"message": null
}Preview a transfer
curl -X POST "$PAYFLOW_CLIENT_API_URL/ach/transfers/preview" \
-H "Authorization: Bearer $PAYFLOW_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amountMinor": "12500",
"currency": "USD",
"metadata": {
"invoiceId": "INV-ACH-1001"
}
}'The preview evaluates ACH routing and returns provider/channel fee information when routing and provider readiness checks pass.
Create a transfer
curl -X POST "$PAYFLOW_CLIENT_API_URL/ach/transfers" \
-H "Authorization: Bearer $PAYFLOW_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"idempotencyKey": "invoice-INV-ACH-1001-v1",
"amountMinor": "12500",
"currency": "USD",
"customerToken": "eyJ...",
"routingNumber": "222222226",
"accountNumber": "123456789",
"bankAccountType": "checking",
"bankName": "Test Bank",
"accountName": "Jane Stone",
"description": "ACH invoice payment",
"metadata": {
"invoiceId": "INV-ACH-1001"
},
"remittanceData": {
"invoice": "INV-ACH-1001"
}
}'Important fields:
| Field | Notes |
|---|---|
idempotencyKey | Required, 8 to 160 characters. Reuse it for safe retries of the same business action. |
amountMinor | Positive smallest currency unit. |
currency | Currently expected to be USD for Dwolla ACH. |
customerToken | Token returned by /ach/customer-tokens. |
routingNumber | Nine digits. |
accountNumber | Four to seventeen digits. |
bankAccountType | checking or savings. |
bankName | Human-readable bank name. |
remittanceData | Optional string or object. |
Idempotency
ACH references are deterministic for an organization, application environment, and idempotencyKey. If your service retries after a timeout, reuse the same key rather than creating a new one.
Failure cases
| Condition | Typical status | Notes |
|---|---|---|
| Missing secret key | 403 | ACH endpoints require secret credentials. |
| Invalid body | 400 | Field validation failed. |
| Routing unavailable | 409 | ACH routing is disabled or incomplete. |
| Provider not configured | 503 | Dwolla credentials or source funding source are missing. |
| Insufficient wallet balance | 409 | Payflow-held wallet does not have enough available funds. |
| Provider transfer rejected | 422 | Dwolla or provider validation rejected the transfer. |
Operational notes
- Store only the Payflow reference, bank last four, provider references, and status in your app.
- Do not log full routing or account numbers.
- Make sure Dwolla source funding source URLs point to a specific
/funding-sources/:idresource, not a collection URL. - Configure Dwolla webhooks so transfer status changes can update Payflow transactions.