ACH Transfers
Tokenize ACH customers, preview transfer cost, and create provider-routed ACH transfers through Payflow.
ACH transfers are server-side flows. They require secret API keys, ACH routing, provider readiness, and valid wallet availability for the target environment. Payflow can route ACH transfers through supported providers such as Dwolla or Moov.
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 the combined service fee when routing and provider readiness checks pass. The fee combines Payflow's provider-cost recovery fee and the organization's configured fee.
Example response:
{
"success": true,
"data": {
"amountMinor": "12500",
"currency": "USD",
"routing": {
"provider": "moov",
"channel": "standard_ach",
"channelLabel": "Standard ACH (1 - 3 days)",
"fallback": false
},
"fee": {
"platformFeeMinor": "25",
"organizationFeeMinor": "100",
"feeMinor": "125"
},
"totalDebitMinor": "12625"
},
"message": null
}Relevant response fields:
| Field | Meaning |
|---|---|
amountMinor | Principal transfer amount. |
fee.platformFeeMinor | Payflow provider-cost recovery fee. |
fee.organizationFeeMinor | Organization-configured fee. |
fee.feeMinor | Combined service fee. |
totalDebitMinor | Principal amount plus combined service fee. |
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 ACH transfers. |
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. |
Response:
{
"success": true,
"data": {
"transaction": {
"payflowReference": "pf_test_ach_69f14d7e1d3b4d5a9cfbb887adfc34c0",
"status": "processing",
"amountMinor": "12500",
"platformFeeMinor": "25",
"organizationFeeMinor": "100",
"feeMinor": "125",
"totalDebitMinor": "12625",
"currency": "USD",
"paymentMethod": "ach",
"provider": "moov",
"channel": "standard_ach"
},
"replayed": false,
"bankAccount": {
"bankName": "Test Bank",
"accountType": "checking",
"last4": "6789"
}
},
"message": null
}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 | ACH provider credentials or source funding details are missing. |
| Insufficient wallet balance | 409 | Payflow-held wallet does not have enough available funds. |
| Provider transfer rejected | 422 | Dwolla, Moov, or provider validation rejected the transfer. |
Operational notes
- Store only the Payflow reference, bank last four, provider references, and status in your app.
- Display the previewed fee and total debit before creating the transfer.
- Do not log full routing or account numbers.
- Make sure provider funding source settings point to a specific usable source account or funding source.
- Configure ACH provider webhooks so transfer status changes can update Payflow transactions.