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 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.
2. Have the investor link their bank¶
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:
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.