Accept an ACH payment
Debit a bank account over the API, understand what an ACH approval promises and what it doesn't, and be ready for the return that can arrive days later.
Signed in, you can run this blueprint against your own sandbox merchant one call at a time, with the values from each call threaded into the next. Sign in to run it.
4 steps, 2 API callsACHPaymentsTransactions
Samples use {{API_KEY}} for your API key and {{BASE_URL}} for this instance's API address. Anything else in double braces is a value an earlier step gave you.
Send the debit
1. Send the sale with check data
API call
POST /api/transactions
Send the same sale request you would send for a card, with a checkData block in place of the card block. The check data is what selects the ACH rail; there is no separate endpoint for it. The SEC code says under which NACHA authorization class you are debiting the account, and PPD is the ordinary choice for a personal account you hold a signed authorization for.
Values this step gives you
{{transactionId}}The id of the debit, from the response body's id property.{{merchantId}}The merchant the debit belongs to, from the response body's merchantId property.
curl -X POST "{{BASE_URL}}/api/transactions" \
-H "api-key: {{API_KEY}}" \
-H "Content-Type: application/json" \
-d '{
"transactionType": "Sale",
"checkData": {
"nameOnCheck": "Jane Doe",
"routingNumber": "021000021",
"accountNumber": "1234567890",
"accountType": "Checking",
"secCode": "Ppd"
},
"invoiceData": {
"amounts": { "base": 25.00, "total": 25.00 }
}
}'using var http = new HttpClient { BaseAddress = new Uri("{{BASE_URL}}") };
http.DefaultRequestHeaders.Add("api-key", "{{API_KEY}}");
var response = await http.PostAsJsonAsync("/api/transactions", new
{
transactionType = "Sale",
checkData = new
{
nameOnCheck = "Jane Doe",
routingNumber = "021000021",
accountNumber = "1234567890",
accountType = "Checking",
secCode = "Ppd"
},
invoiceData = new
{
amounts = new { @base = 25.00m, total = 25.00m }
}
});
response.EnsureSuccessStatusCode();
var debit = await response.Content.ReadFromJsonAsync<JsonElement>();
var transactionId = debit.GetProperty("id").GetString();
var merchantId = debit.GetProperty("merchantId").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "9f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f",
"merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
"transactionType": "Sale",
"resultCode": "Ok",
"authorizedAmount": 25.00,
"creationTime": "2026-02-04T18:22:41.517Z",
"responseData": {
"resultCode": "Ok",
"resultMessage": "ACH Sale Approved",
"secCode": "Ppd"
}
}2. Read the outcome off the response
On your side
The sandbox answers with the result code "Ok" and the message "ACH Sale Approved" in the response body. Read that as acceptance into the ACH network, not as a card-style authorization: no issuer checked a balance and no funds are held. The debit now clears through the network on its own schedule, and it can still come back as a return days later. Treat an accepted debit as money in flight rather than money received.
Values this step gives you
What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "{{transactionId}}",
"merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
"transactionType": "Sale",
"resultCode": "Ok",
"authorizedAmount": 25.00,
"creationTime": "2026-02-04T18:22:41.517Z",
"responseData": {
"resultCode": "Ok",
"resultMessage": "ACH Sale Approved",
"secCode": "Ppd"
}
}Confirm what was accepted
3. Read the transaction back
API call
GET /api/transactions/{{transactionId}}
Read the debit you just created. The create response and the stored transaction are the same record, and this record is the one a later return lands on. Reconcile against it rather than against the create response alone, so a request that times out on your side still has somewhere to recover the outcome from.
Values this step gives you
curl "{{BASE_URL}}/api/transactions/{{transactionId}}" \
-H "api-key: {{API_KEY}}"var transaction = await http.GetFromJsonAsync<JsonElement>(
$"/api/transactions/{transactionId}");
var result = transaction.GetProperty("responseData")
.GetProperty("resultMessage").GetString();What this step answers with
Abridged to the properties this step depends on. A real response carries more.
{
"id": "{{transactionId}}",
"merchantId": "3a7b1c9d-2e4f-4a6b-8c8d-9e0f1a2b3c4d",
"transactionType": "Sale",
"resultCode": "Ok",
"authorizedAmount": 25.00,
"creationTime": "2026-02-04T18:22:41.517Z",
"responseData": {
"resultCode": "Ok",
"resultMessage": "ACH Sale Approved",
"secCode": "Ppd"
}
}Be ready for the return
4. Handle the return that arrives later
On your side
On the live rail a return arrives days after the debit was accepted, long after this flow has finished. Build your integration so a transaction can leave an accepted state and enter a returned one: the record you read back above is the one the NACHA return code lands on. The return scenario below collapses that wait to a single sandbox call so you can prove the path now, and the webhook blueprint is how your system hears about a return without polling for it.