ACH

Setting Up ACH Processing

Learn how to set up ACH processing from scratch

In this tutorial, we will set up ACH processing on the Twisp platform. By the end, you will have a fully configured ACH processor ready to send and receive ACH transactions.

What We'll Build

By completing this tutorial, we will:

  • Create all required accounts for ACH processing
  • Set up a journal for ACH transactions
  • Configure a webhook endpoint for transaction decisioning
  • Create an ACH configuration
  • Verify everything works correctly

This tutorial takes approximately 15 minutes to complete.

Prerequisites

Before we begin, you need:

  • A Twisp account with API access
  • Your GraphQL API endpoint URL
  • API credentials (authentication token)

You do NOT need:

  • An ODFI relationship (that comes later for production)
  • Understanding of double-entry accounting
  • Prior ACH experience

Step 1: Create the Journal

We will start by creating a journal where all ACH transactions will be posted.

mutation CreateJournal {
  createJournal(
    input: {
      journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a"
      name: "ACH Processing Journal"
      status: ACTIVE
    }
  ) {
    journalId
    name
    status
  }
}

What Just Happened: We created a journal with ID 8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a. This journal will track all ACH-related accounting entries.

Expected Response:

{
  "data": {
    "createJournal": {
      "journalId": "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a",
      "name": "ACH Processing Journal",
      "status": "ACTIVE"
    }
  }
}

Step 2: Create Required Accounts

Next, we will create four accounts required for ACH processing. We'll create them all at once.

mutation CreateACHAccounts {
  # Settlement account - where funds transit
  settlement: createAccount(
    input: {
      accountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea"
      code: "settlement.ach"
      name: "ACH Settlement"
      normalBalanceType: DEBIT
      config: {
        enableConcurrentPosting: true
      }
    }
  ) {
    accountId
    name
  }

  # Suspense account - for transactions to unknown accounts
  suspense: createAccount(
    input: {
      accountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf"
      code: "suspense.ach"
      name: "ACH Suspense"
      config: {
        enableConcurrentPosting: true
      }
    }
  ) {
    accountId
    name
  }

  # Exception account - for failed transactions
  exception: createAccount(
    input: {
      accountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c"
      code: "exception.ach"
      name: "ACH Exception"
      config: {
        enableConcurrentPosting: true
      }
    }
  ) {
    accountId
    name
  }

  # Fee account - for ACH processing fees
  fee: createAccount(
    input: {
      accountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d"
      code: "fee.ach"
      name: "ACH Fee Income"
      normalBalanceType: CREDIT
      config: {
        enableConcurrentPosting: true
      }
    }
  ) {
    accountId
    name
  }
}

What Just Happened: We created four accounts:

  1. Settlement Account (37f7e8a6-171f-411d-ad59-7b1f40f505ea) - All ACH funds flow through this account during processing
  2. Suspense Account (3171b0c2-6e9f-41aa-a5a6-ee927deb27cf) - Holds transactions when the destination account can't be found
  3. Exception Account (4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c) - Receives transactions that fail processing rules
  4. Fee Account (5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d) - Collects ACH processing fees

All accounts have enableConcurrentPosting: true to support high transaction volumes.

Expected Response:

{
  "data": {
    "settlement": {
      "accountId": "37f7e8a6-171f-411d-ad59-7b1f40f505ea",
      "name": "ACH Settlement"
    },
    "suspense": {
      "accountId": "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf",
      "name": "ACH Suspense"
    },
    "exception": {
      "accountId": "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c",
      "name": "ACH Exception"
    },
    "fee": {
      "accountId": "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d",
      "name": "ACH Fee Income"
    }
  }
}

Step 3: Create the Webhook Endpoint

Now we will create a webhook endpoint. This endpoint will receive requests for transaction decisioning when ACH files are processed.

For this tutorial, we'll use a test endpoint URL. In production, you'll replace this with your actual webhook server.

mutation CreateWebhook {
  events {
    createEndpoint(
      input: {
        endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb"
        status: ENABLED
        endpointType: ACH_PROCESSOR
        url: "https://webhook.site/unique-url-here"
        subscription: []
        description: "ACH decisioning webhook"
      }
    ) {
      endpointId
      url
      status
    }
  }
}

What Just Happened: We created a webhook endpoint with ID b84512f1-a67e-4dc2-94dd-66c48b4d13fb. When ACH files are processed, Twisp will send POST requests to the URL we specified.

For This Tutorial: Use https://webhook.site to create a free test webhook URL:

  1. Go to https://webhook.site
  2. Copy the unique URL shown
  3. Use that URL in the mutation above

Expected Response:

{
  "data": {
    "events": {
      "createEndpoint": {
        "endpointId": "b84512f1-a67e-4dc2-94dd-66c48b4d13fb",
        "url": "https://webhook.site/your-unique-url",
        "status": "ENABLED"
      }
    }
  }
}

Step 4: Create the ACH Configuration

Now we will tie everything together by creating an ACH configuration. This tells the ACH processor which accounts to use and where to send webhooks.

mutation CreateACHConfig {
  ach {
    createConfiguration(
      input: {
        configId: "fe27128a-b331-4e0e-94f8-9a32443fee36"
        endpointId: "b84512f1-a67e-4dc2-94dd-66c48b4d13fb"
        journalId: "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a"
        settlementAccountId: "37f7e8a6-171f-411d-ad59-7b1f40f505ea"
        suspenseAccountId: "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf"
        exceptionAccountId: "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c"
        feeAccountId: "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d"
        odfiHeaderConfiguration: {
          immediateDestination: "021000021"
          immediateDestinationName: "Test Bank"
          immediateOrigin: "1234567890"
          immediateOriginName: "Your Company"
        }
        timeZone: "America/New_York"
      }
    ) {
      configId
      version
      timeZone
    }
  }
}

What Just Happened: We created an ACH configuration that connects:

  • The journal we created
  • All four accounts
  • The webhook endpoint
  • ODFI header information for generating ACH files

The odfiHeaderConfiguration contains placeholder values. When you're ready for production, you'll replace these with real values from your ODFI (bank).

Expected Response:

{
  "data": {
    "ach": {
      "createConfiguration": {
        "configId": "fe27128a-b331-4e0e-94f8-9a32443fee36",
        "version": 1,
        "timeZone": "America/New_York"
      }
    }
  }
}

Step 5: Verify the Configuration

Let's verify everything was created correctly by querying our configuration.

query VerifySetup {
  ach {
    configuration(
      id: "fe27128a-b331-4e0e-94f8-9a32443fee36"
    ) {
      configId
      journalId
      settlementAccountId
      suspenseAccountId
      exceptionAccountId
      feeAccountId
      endpointId
      odfiHeaderConfiguration {
        immediateDestination
        immediateOrigin
      }
      timeZone
      version
    }
  }
}

Expected Response:

{
  "data": {
    "ach": {
      "configuration": {
        "configId": "fe27128a-b331-4e0e-94f8-9a32443fee36",
        "journalId": "8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a",
        "settlementAccountId": "37f7e8a6-171f-411d-ad59-7b1f40f505ea",
        "suspenseAccountId": "3171b0c2-6e9f-41aa-a5a6-ee927deb27cf",
        "exceptionAccountId": "4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c",
        "feeAccountId": "5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d",
        "endpointId": "b84512f1-a67e-4dc2-94dd-66c48b4d13fb",
        "odfiHeaderConfiguration": {
          "immediateDestination": "021000021",
          "immediateOrigin": "1234567890"
        },
        "timeZone": "America/New_York",
        "version": 1
      }
    }
  }
}

What to Check:

  • All IDs match what we created
  • Version is 1 (this is the first version)
  • Timezone is correct for your location

If everything matches, congratulations! Your ACH processor is configured.

What We Accomplished

Let's review what we built:

  1. ✅ Created a journal for ACH transactions
  2. ✅ Created four required accounts (settlement, suspense, exception, fee)
  3. ✅ Set up a webhook endpoint
  4. ✅ Created an ACH configuration connecting everything
  5. ✅ Verified the configuration is correct

Save These IDs

You'll need these IDs going forward. Save them somewhere safe:

Journal ID:     8d7e6f5a-4b3c-2d1e-0f9a-8b7c6d5e4f3a
Settlement:     37f7e8a6-171f-411d-ad59-7b1f40f505ea
Suspense:       3171b0c2-6e9f-41aa-a5a6-ee927deb27cf
Exception:      4a8f2b1e-3c9d-4f7e-a5b6-1d8e9f0a2b3c
Fee:            5b9e3c2f-4d0e-5a8f-b6c7-2e9f0a1b3c4d
Webhook:        b84512f1-a67e-4dc2-94dd-66c48b4d13fb
Config ID:      fe27128a-b331-4e0e-94f8-9a32443fee36

Next Steps

Now that your ACH processor is set up, you're ready to send your first ACH payment!

Continue to the next tutorial:

Production Checklist: Before using ACH in production, you'll need to:

  • [ ] Establish a relationship with an ODFI (bank)
  • [ ] Get real ODFI header values from your bank
  • [ ] Update the odfiHeaderConfiguration with real values
  • [ ] Set up SFTP/FTPS for file transmission
  • [ ] Implement a production webhook server
  • [ ] Test with your ODFI's validation process

Troubleshooting

Problem: "Account already exists" error

If you see this error, it means you already have an account with that ID. This is fine! You can either:

  • Use the existing accounts and skip account creation
  • Choose different UUIDs for your accounts

Problem: "Webhook endpoint creation failed"

Common causes:

  • Invalid URL format - make sure it starts with https://
  • Network issue - try again in a moment

Problem: "Configuration creation failed"

Check that:

  • All referenced IDs (journal, accounts, endpoint) exist
  • You haven't already created a configuration with this ID
  • All UUIDs are properly formatted

Summary

You've successfully set up ACH processing! We created:

  • A journal for transaction tracking
  • Four accounts for different processing scenarios
  • A webhook endpoint for transaction decisioning
  • An ACH configuration tying everything together

You're now ready to process ACH transactions. The next tutorial will show you how to send your first payment.