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
transactionTypeset toACH. See Create the Trade. To switch an existingCREATEDtrade to ACH, callupdateTradeTransactionType. - 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.
2. Have the investor link their bank¶
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:
linkExternalAccountandupdateLinkExternalAccount: 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'sfundStatuschanged.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.