Skip to content

ACH

An ACH payment debits the investor's bank account and moves the funds into the offering's escrow account. First you attach a bank account to the investor's account, either through Plaid or with bank details the investor enters. Then you call externalFundMove against the trade. Use this page when the trade's transactionType is ACH.

Before You Start

  • Register webhooks so you receive the linking and payment status events.
  • The investor has an approved account (accountId), as described in Onboard Investors.
  • A trade exists for that account with transactionType set to ACH. See Create the Trade. To switch an existing CREATED trade to ACH, call updateTradeTransactionType.
  • For Plaid linking, the investor needs a browser to complete the Plaid flow. TransactAPI holds the Plaid credentials, so you need no Plaid account of your own.
  • Plaid linking, ACH debits, failed ACH returns and chargebacks are billed in production. See the Fee Schedule.

An account can have only one external bank account. Link it once and reuse it for later trades.

Steps

1. Start Plaid bank linking

Call linkExternalAccount for the investor's account. TransactAPI returns a URL for a hosted page that runs Plaid Link, so you need no Plaid account or Plaid integration of your own.

curl -X POST "$TAPI_HOST/v3/linkExternalAccount" \
  -H "Authorization: Bearer $TAPI_CLIENT_ID:$TAPI_API_KEY" \
  -d accountId=A12345
{
  "statusCode": "101",
  "statusDesc": "Ok",
  "accountDetails": "https://api-sandboxdash.norcapsecurities.com/..."
}

Treat accountDetails as an opaque URL. Don't parse or build it yourself, and don't store it; request a new one each time.

The call fails with 715 or 716 if the account already has a bank account. To replace an existing bank account, call updateLinkExternalAccount instead. It returns the same kind of URL, or 720 if there is nothing to replace.

Open the accountDetails URL for the investor, for example in a new window or an embedded frame. The investor signs in to their bank through Plaid and selects a checking or savings account. TransactAPI then retrieves and validates the routing and account numbers and saves them as the account's external bank account.

When linking completes, you receive the linkExternalAccount webhook (or updateLinkExternalAccount when replacing), and the account is ready to debit. To confirm on demand, call getExternalAccount.

Manual bank details instead of Plaid. If the investor types in their bank details, call createExternalAccount with types=Account in place of steps 1 and 2:

curl -X POST "$TAPI_HOST/v3/createExternalAccount" \
  -H "Authorization: Bearer $TAPI_CLIENT_ID:$TAPI_API_KEY" \
  -d types=Account \
  -d accountId=A12345 \
  -d ExtAccountfullname="Jane Smith" \
  -d Extnickname="Jane Checking" \
  -d ExtRoutingnumber=011401533 \
  -d ExtAccountnumber=1111222233330000 \
  -d accountType=Checking \
  -d updatedIpAddress=10.0.0.1

The response returns the saved record under External Account Details, with the routing and account numbers Base64-encoded. Extnickname rejects some special characters; keep it to letters, digits, spaces, hyphens, and underscores.

3. Debit the account with externalFundMove

Start the ACH debit for the trade. It debits the single external account on file for accountId, so no bank details are sent.

curl -X POST "$TAPI_HOST/v3/externalFundMove" \
  -H "Authorization: Bearer $TAPI_CLIENT_ID:$TAPI_API_KEY" \
  -d accountId=A12345 \
  -d offeringId=12345 \
  -d tradeId=123456789 \
  -d amount=12345.00 \
  -d description="Investment in Example Offering" \
  -d checkNumber=123456789 \
  -d createdIpAddress=10.0.0.1
{
  "statusCode": "101",
  "statusDesc": "Ok",
  "TradeFinancialDetails": [
    {
      "accountId": "A12345",
      "tradeId": "123456789",
      "offeringId": "12345",
      "totalAmount": "12345.000000",
      "RefNum": "987654321",
      "fundStatus": "Pending"
    }
  ]
}

Keep RefNum. It identifies this payment in status webhooks, lookups and void requests. checkNumber must be present but is not used, so pass the trade ID. amount may exceed the trade total by up to 7% to cover fees. A trade can have only one live fund move: another request fails with 150 until the previous one is Returned or Voided. See externalFundMove for all preconditions and limits.

In Sandbox, you can drive a payment straight to Settled or Returned by giving the external account a reserved nickname. See Simulating ACH outcomes in Sandbox.

4. Track the payment status

Pending debits are submitted for processing at 6:00 PM Eastern Time each business day and settle or return within 3 to 5 business days. Each status change sends the updateExternalFundMoveStatus webhook with RefNum, tradeId and the new fundStatus. When the payment settles, the trade moves to FUNDED and the updateTradeStatus webhook follows.

To check on demand, read the trade:

curl -X GET "$TAPI_HOST/v3/trades/123456789" \
  -H "Authorization: Bearer $TAPI_CLIENT_ID:$TAPI_API_KEY"
{
  "statusCode": "101",
  "statusDesc": "Ok",
  "trade": {
    "tradeId": "123456789",
    "accountId": "A12345",
    "transactionType": "ACH",
    "totalAmount": "12345.000000",
    "tradeStatus": "CREATED",
    "paymentStatus": "Submitted"
  }
}

paymentStatus is the fund move's status. getExternalFundMoveInfo returns the full fund move record, including any return code in error.

5. Void a pending payment (optional)

To cancel a debit before it is submitted, void it with its RefNum. Only Pending payments can be voided.

curl -X POST "$TAPI_HOST/v3/requestForVoidACH" \
  -H "Authorization: Bearer $TAPI_CLIENT_ID:$TAPI_API_KEY" \
  -d RefNum=987654321
{
  "statusCode": "101",
  "statusDesc": "Ok",
  "investorExternalAccountDetails": "Status Updated Successfully"
}

The payment moves to Voided and the requestForVoidACH webhook fires. The trade stays CREATED, so you can call externalFundMove again.

Webhooks

These ACH and external account webhooks fire in this step:

  • linkExternalAccount and updateLinkExternalAccount: the investor finished linking or replacing a bank account through Plaid.
  • createExternalAccount: bank details were added manually.
  • externalFundMove: an ACH debit was started.
  • updateExternalFundMoveStatus: the debit's fundStatus changed.
  • requestForVoidACH: a pending debit was voided.

When the trade becomes FUNDED, the updateTradeStatus webhook fires.

Common Errors

See Error Codes for the full list.

Code When it happens here
715, 716 linkExternalAccount: the account already has a bank account. Use updateLinkExternalAccount to replace it.
116 createExternalAccount: the account already has an external bank account.
720 updateLinkExternalAccount: the account has no bank account to replace.
149 externalFundMove found no complete external bank account for accountId. Link one first.
150 The trade already has a Pending, Submitted or Settled fund move.
194 The trade's transactionType is not ACH.
215 The bank's routing number is not a valid ABA routing number.

Next

When the trade is FUNDED, continue to Settle and Manage.