No such blueprint
This instance publishes no blueprint at that address. The catalog lists every one it does publish.
Back to the blueprints
Blueprints
/
Full integrations
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 .
On this page
4 steps, 2 API calls ACH Payments Transactions
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.
Reference for this operation
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 CopycURL
Copied
Copy failed
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 }
}
}'.NET Copy.NET
Copied
Copy failed
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.
HTTP 200 CopyHTTP 200
Copied
Copy failed
{
"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.
Reference for this operation
Values this step gives you
What this step answers with
Abridged to the properties this step depends on. A real response carries more.
HTTP 200 CopyHTTP 200
Copied
Copy failed
{
"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.
Reference for this operation
Values this step gives you
cURL CopycURL
Copied
Copy failed
curl "{{BASE_URL}}/api/transactions/{{transactionId}}" \
-H "api-key: {{API_KEY}}".NET Copy.NET
Copied
Copy failed
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.
HTTP 200 CopyHTTP 200
Copied
Copy failed
{
"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.
Reference for this operation
Simulate an ACH return
Receive and verify webhooks
Values this step gives you
Run the whole flow as one script
Every step above in one script you can copy and run. Replace {{API_KEY}} with your own API key and {{BASE_URL}} with this instance's API address, and set any value the script asks you for at the top. A step that happens outside the API stays a comment.
Code sample language >_
cURL PS
PowerShell TS
TypeScript C#
C# Py
Python
brew install jq
Copythe install command for cURL
Copied
Copy failed
cURL CopycURL
Copied
Copy failed
#!/usr/bin/env bash
# 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.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.
set -euo pipefail
BASE_URL="{{BASE_URL}}"
API_KEY="{{API_KEY}}"
# Sends one request and prints the response body. A failed call prints the API's answer and stops
# the script.
call() {
local method="$1" path="$2" body="${3:-}" out
local args=(-sS --fail-with-body -X "$method" "$BASE_URL$path" -H "api-key: $API_KEY")
if [ -n "$body" ]; then
args+=(-H "Content-Type: application/json" -d "$body")
fi
if ! out=$(curl "${args[@]}"); then
printf '%s\n' "$out" >&2
return 1
fi
printf '%s' "$out"
}
# Percent-encodes a value for use in a URL.
urlencode() {
jq -rn --arg value "$1" '$value | @uri'
}
# Phase 1: Send the debit
# Step 1: Send the sale with check data
step1=$(call POST "/api/transactions" '{
"transactionType": "Sale",
"checkData": {
"nameOnCheck": "Jane Doe",
"routingNumber": "021000021",
"accountNumber": "1234567890",
"accountType": "Checking",
"secCode": "Ppd"
},
"invoiceData": {
"amounts": { "base": 25.00, "total": 25.00 }
}
}')
transactionId=$(jq -er '.id | if . == null then error("The response carried no value for transactionId.") else tostring end' <<< "$step1")
merchantId=$(jq -er '.merchantId | if . == null then error("The response carried no value for merchantId.") else tostring end' <<< "$step1")
# Step 2: Read the outcome off the response
# 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.
# Phase 2: Confirm what was accepted
# Step 3: Read the transaction back
call GET "/api/transactions/$(urlencode "$transactionId")" > /dev/null
# Phase 3: Be ready for the return
# Step 4: Handle the return that arrives later
# 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.
Copythe install command for PowerShell
Copied
Copy failed
PowerShell CopyPowerShell
Copied
Copy failed
# 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.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.
$ErrorActionPreference = 'Stop'
$baseUrl = '{{BASE_URL}}'
$apiKey = '{{API_KEY}}'
# Sends one request and returns the parsed response body. A failed call stops the script.
function Invoke-BlueprintCall([string] $Method, [string] $Path, [string] $Body) {
$arguments = @{
Method = $Method
Uri = $baseUrl + $Path
Headers = @{ 'api-key' = $apiKey }
}
if ($Body) {
$arguments.ContentType = 'application/json'
$arguments.Body = [System.Text.Encoding]::UTF8.GetBytes($Body)
}
Invoke-RestMethod @arguments
}
# Turns a value read off a response back into the text the API sent. A missing value stops the
# script.
function ConvertTo-CaptureValue($Value, [string] $Name) {
if ($null -eq $Value) { throw "The response carried no value for $Name." }
if ($Value -is [datetime] -and $Value.Kind -eq 'Unspecified') { return $Value.ToString('yyyy-MM-ddTHH:mm:ss.FFFFFFF', [cultureinfo]::InvariantCulture) }
if ($Value -is [datetime]) { return $Value.ToUniversalTime().ToString('o') }
if ($Value -is [bool]) { return $Value.ToString().ToLowerInvariant() }
[string]$Value
}
# Phase 1: Send the debit
# Step 1: Send the sale with check data
$body = @'
{
"transactionType": "Sale",
"checkData": {
"nameOnCheck": "Jane Doe",
"routingNumber": "021000021",
"accountNumber": "1234567890",
"accountType": "Checking",
"secCode": "Ppd"
},
"invoiceData": {
"amounts": { "base": 25.00, "total": 25.00 }
}
}
'@
$step1 = Invoke-BlueprintCall -Method 'POST' -Path '/api/transactions' -Body $body
$transactionId = ConvertTo-CaptureValue $step1.id 'transactionId'
$merchantId = ConvertTo-CaptureValue $step1.merchantId 'merchantId'
# Step 2: Read the outcome off the response
# 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.
# Phase 2: Confirm what was accepted
# Step 3: Read the transaction back
$null = Invoke-BlueprintCall -Method 'GET' -Path "/api/transactions/$([uri]::EscapeDataString($transactionId))"
# Phase 3: Be ready for the return
# Step 4: Handle the return that arrives later
# 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.
Copythe install command for TypeScript
Copied
Copy failed
TypeScript CopyTypeScript
Copied
Copy failed
// 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.
//
// Every API call in this blueprint, in order. Each value a call returns is passed to the calls
// after it. A step that happens outside the API is a comment, and any failed call stops the script.
export {};
const baseUrl = '{{BASE_URL}}';
const apiKey = '{{API_KEY}}';
// Sends one request and returns the parsed response body. A failed call throws.
async function call(method: string, path: string, body?: unknown): Promise<any> {
const headers: Record<string, string> = { 'api-key': apiKey };
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
const response = await fetch(baseUrl + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`${method} ${path} answered ${response.status}: ${text}`);
}
return text ? JSON.parse(text) : null;
}
// Turns a value read off a response into the text the API sent. A missing value throws.
function capture(value: unknown, name: string): string {
if (value === undefined || value === null) {
throw new Error(`The response carried no value for ${name}.`);
}
return typeof value === 'string' ? value : JSON.stringify(value);
}
// Phase 1: Send the debit
// Step 1: Send the sale with check data
const step1 = await call('POST', '/api/transactions', {
"transactionType": "Sale",
"checkData": {
"nameOnCheck": "Jane Doe",
"routingNumber": "021000021",
"accountNumber": "1234567890",
"accountType": "Checking",
"secCode": "Ppd"
},
"invoiceData": {
"amounts": { "base": 25.00, "total": 25.00 }
}
});
const transactionId = capture(step1.id, 'transactionId');
const merchantId = capture(step1.merchantId, 'merchantId');
// Step 2: Read the outcome off the response
// 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.
// Phase 2: Confirm what was accepted
// Step 3: Read the transaction back
await call('GET', `/api/transactions/${encodeURIComponent(transactionId)}`);
// Phase 3: Be ready for the return
// Step 4: Handle the return that arrives later
// 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.
Copythe install command for C#
Copied
Copy failed
C# CopyC#
Copied
Copy failed
// 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.
//
// Every API call in this blueprint, in order. Each value a call returns is passed to the calls
// after it. A step that happens outside the API is a comment, and any failed call stops the script.
using System.Text;
using System.Text.Json;
var baseUrl = "{{BASE_URL}}";
var apiKey = "{{API_KEY}}";
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("api-key", apiKey);
// Sends one request and returns the parsed response body. A failed call throws.
async Task<JsonElement> CallAsync(string method, string path, string? body = null)
{
using var request = new HttpRequestMessage(new HttpMethod(method), baseUrl + path);
if (body is not null)
{
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
using var response = await http.SendAsync(request);
var json = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException($"{method} {path} answered {(int)response.StatusCode}: {json}");
}
return json.Length == 0 ? default : JsonSerializer.Deserialize<JsonElement>(json);
}
// Turns a value read off a response into the text the API sent. A missing value throws.
static string Capture(JsonElement value, string name) => value.ValueKind switch
{
JsonValueKind.String => value.GetString()!,
JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.GetRawText(),
_ => throw new InvalidOperationException($"The response carried no value for {name}.")
};
// Phase 1: Send the debit
// Step 1: Send the sale with check data
var step1 = await CallAsync("POST", "/api/transactions", """
{
"transactionType": "Sale",
"checkData": {
"nameOnCheck": "Jane Doe",
"routingNumber": "021000021",
"accountNumber": "1234567890",
"accountType": "Checking",
"secCode": "Ppd"
},
"invoiceData": {
"amounts": { "base": 25.00, "total": 25.00 }
}
}
""");
var transactionId = Capture(step1.GetProperty("id"), "transactionId");
var merchantId = Capture(step1.GetProperty("merchantId"), "merchantId");
// Step 2: Read the outcome off the response
// 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.
// Phase 2: Confirm what was accepted
// Step 3: Read the transaction back
await CallAsync("GET", $"/api/transactions/{Uri.EscapeDataString(transactionId)}");
// Phase 3: Be ready for the return
// Step 4: Handle the return that arrives later
// 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.
pip install requests
Copythe install command for Python
Copied
Copy failed
Python CopyPython
Copied
Copy failed
# 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.
#
# Every API call in this blueprint, in order. Each value a call returns is passed to the calls after
# it. A step that happens outside the API is a comment, and any failed call stops the script.
from urllib.parse import quote
import json
import requests
BASE_URL = "{{BASE_URL}}"
API_KEY = "{{API_KEY}}"
# Sends one request and returns the parsed response body. A failed call raises.
def call(method, path, body=None):
response = requests.request(
method,
BASE_URL + path,
headers={"api-key": API_KEY},
json=body,
)
response.raise_for_status()
return response.json() if response.content else None
# Turns a value read off a response into the text the API sent. A missing value raises.
def capture(value, name):
if value is None:
raise ValueError(f"The response carried no value for {name}.")
return value if isinstance(value, str) else json.dumps(value)
# Phase 1: Send the debit
# Step 1: Send the sale with check data
step1 = call("POST", "/api/transactions", {
"transactionType": "Sale",
"checkData": {
"nameOnCheck": "Jane Doe",
"routingNumber": "021000021",
"accountNumber": "1234567890",
"accountType": "Checking",
"secCode": "Ppd"
},
"invoiceData": {
"amounts": { "base": 25.00, "total": 25.00 }
}
})
transaction_id = capture(step1["id"], "transactionId")
merchant_id = capture(step1["merchantId"], "merchantId")
# Step 2: Read the outcome off the response
# 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.
# Phase 2: Confirm what was accepted
# Step 3: Read the transaction back
call("GET", f"/api/transactions/{quote(transaction_id, safe='')}")
# Phase 3: Be ready for the return
# Step 4: Handle the return that arrives later
# 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.
Next steps