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 web browser to complete the Plaid flow (see Where the hosted page runs). 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.

Change on November 1, 2026

Starting November 1, 2026, each URL returned by linkExternalAccount and updateLinkExternalAccount is valid for 24 hours after it is generated and for one successfully linked bank account. Reopening the page within that time still works, including after a failed Plaid attempt. After the bank account is linked, request a new URL to link or replace one. The params value becomes a fixed 64-character string. URLs generated before November 1 keep working for 24 hours after the change.

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 in the investor's browser in a new window or tab. 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.

Result in the browser when you open a new window or tab. If your page opens the URL with window.open, the hosted page posts one message back to your page when the investor finishes, cancels, or hits an error. The message is a JSON string:

{
  "source": "nc-plaid-link",
  "status": "success",
  "message": "Account linked successfully!",
  "accountId": "A12345"
}

status is success, error, or cancelled. The message carries no bank details. accountId is omitted when the page fails before it can identify the account, for example when the URL is invalid. Listen for it with window.addEventListener('message', ...), ignore messages whose event.origin is not your TransactAPI host, and treat the webhook or getExternalAccount as the record of the linked account. The window stays open after the investor finishes. If the investor cancels or Plaid reports an error, the page usually offers a Try again button, so a later success message can follow a cancelled or error one.

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.

Where the hosted page runs

The hosted page is a web page, and it must load from TransactAPI in the investor's browser. Don't fetch it on your server and return its contents.

Environment Supported
Desktop or mobile web browser, new window or tab Yes (recommended)
Iframe on your web page Yes, but not recommended
In-app webview in a native mobile app (WKWebView, Android WebView) No

Plaid recommends against launching Link from within an iframe: conversion for banks that use OAuth sign-in is lower, and page sizing and state can suffer.

Plaid does not support Link inside in-app webviews, and banks that use OAuth sign-in block their login pages there. If your native iOS or Android app needs Plaid bank linking, integrate Plaid directly with Plaid's iOS and Android SDKs. Contact techsupport@northcapital.com before you start; a direct Plaid integration is reviewed during certification.

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.