View as Markdown

llms.txt

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.

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.

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

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

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

{
  "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

    {
      "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

      curl "{{BASE_URL}}/api/transactions/{{transactionId}}" \
        -H "api-key: {{API_KEY}}"
      .NET

      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

      {
        "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

      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

        brew install jq

        cURL

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

        PowerShell

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

        TypeScript

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

        C#

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

        Python

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

        Reconnecting to the server

        Could not reconnect

        This session has ended

        Attempt 1

        Your work on this page is still here. Retrying keeps it; reloading starts the page again.

        The server no longer holds this page's state, so it has to be loaded again.