Originating ACH Payments

In this tutorial we explore originating an ACH push payment from a subledger account. We will also register relevant webhooks to receive status updates on our payment. Before starting the tutorial, please make sure you have the following:

  • API credentials
  • Partner ID
  • Master Account Number

Get an Access Token

Before calling the COS API you must first request an access token. Once the access token has been obtained it is recommended that it is cached for the lifetime of the token. If the API starts to return a 401 response code, it is likely that your token has expired and you need to obtain a new one. More details can be found here.

Register Webhooks

Webhooks allow us to receive notifications when certain events occur within COS. After we originate our ACH payment in the step below, we will want to stay informed when it has been accepted by the Federal Reserve. For that you will need to call the webhook registration endpoint and indicate you wish to receive ACH.Payment.Sent events:

POST /webhooks/v1/registrations
{
  "partnerId": "00000000-0000-0000-0000-000000000000",
  "eventName": "ACH.Payment.Sent",
  "registrationType": "Push", 
  "callbackUrl": "https://cos.yourcompanysite.com/ach-events"
}

Next, let's register for ACH.Return.Received to be notified in the event our payment gets returned by the receiving bank:

POST /webhooks/v1/registrations
{
  "partnerId": "00000000-0000-0000-0000-000000000000",
  "eventName": "ACH.Return.Received",
  "registrationType": "Push", 
  "callbackUrl": "https://cos.yourcompanysite.com/ach-events"
}

To learn more about webhook registrations click here. For a full listing of available ACH webhook events click here.

Open Subledger

This tutorial assumes you have already obtained a master account which supports subledgers. Alternately, this step can be skipped if you wish to originate a payment directly from your master account. The primary benefit of originating a payment from a subledger is to make reconciliation easier. For example, you could open a subledger for each member of your service and accurately track payment activity for them. The benefits are even greater if you wish to receive payments from other banks directly to these subledgers.

To learn more about master accounts and subledgers click here. Here's an example of the request to open a subledger:

POST /core/v1/dda/subaccounts
{
  "masterAccountNumber": "2001231234",
  "title": "Acme Co",
  "beneficiary": {
    "referenceId": "ABC789",
    "entityName": "Acme Co",
    "streetAddress1": "400 Business Street",
    "streetAddress2": "Suite 123",
    "city": "New York",
    "state": "NY",
    "postalCode": "10025",
    "countryCode": "US",
    "phoneNumber": "2015551234",
    "emailAddress": "[email protected]"
  }
}

And here's an example of a response to the above request:

{
  "accountNumber": "300012341234",
  "accountType": "Deposit",
  "status": "Active",
  "ledgerType": "Passthrough",
  "productType": "Deposit",
  "title": "Acme Co",
  "currentBalance": 0,
  "availableBalance": 0,
  "holdAmount": 0,
  "transactionCount": 0,
  "openedAt": "2020-03-10T04:29:29.182Z",
  "masterAccountNumber": "2001231234",
  "beneficiary": {
    "referenceId": "ABC789",
    "entityName": "Acme Co",
    "streetAddress1": "400 Business Street",
    "streetAddress2": "Suite 123",
    "city": "New York",
    "state": "NY",
    "postalCode": "10025",
    "countryCode": "US",
    "phoneNumber": "2015551234",
    "emailAddress": "[email protected]"
  },
  "partnerId": "00000000-0000-0000-0000-000000000000",
  "lastMaintenanceAt": "2020-03-10T04:29:29.182Z",
  "productId": "00000000-0000-0000-0000-000000000000",
  "lastModifiedAt": "2020-03-10T04:29:29.182Z"
}

Originate Payment

When we send money from COS to another bank, this is called a Push payment. If we were to take money from an account held at another bank, that would be a Pull payment. For the purposes of this tutorial we will be sending money, so the paymentType field should be set to Push.

Now let's send $100 to Bob Smith who has an account at another bank. We will pass the subledger number from the previous step in the account number field. This indicates the account that will be funding the payment.

The receiver fields relate to the account at the other bank. The routingNumber is used by the Federal Reserve to route the payment instruction to the appropriate bank. The accountNumber and name fields will be used by the bank to locate the account you wish to send money to. Lastly, the identification field is a reference value that may or may not appear on the account statement description. For example, this could be a customer reference code or invoice number. Each Financial Institution controls what is displayed on their customers' bank statements.

The serviceType field supports either SameDay or Standard values, and indicates when the payment is effective. Note that SameDay is only supported on domestic payments $1,000,000 or less.

POST /ach/v1/payments
{
  "accountNumber": "300012341234",
  "receiver": {
    "routingNumber": "021000021",
    "accountNumber": "456789000",
    "accountType": "Checking",
    "name": "Bob Smith",
    "identification": "XYZ123",
  },
  "secCode": "WEB",
  "description": "Payment",
  "transactionType": "Push",
  "amount": 10000,
  "serviceType": "SameDay",
}

🚧

It is highly recommended that you include an idempotency key in your request header. This will provide duplicate protection in the event of a failure. Read more about idempotency keys here.

For additional information regarding mapping of COS ACH and NACHA fields, click here.

Payment Confirmation

After originating a payment, it will typically stay in a Pending or Hold status for minutes to hours. It may take up to a few days to transition in the case of weekends and bank holidays. A status of Hold indicates the payment is being reviewed by our Ops team and has not yet been approved for release to the Federal Reserve. A status of Pending lets you know that the payment is good to go, but has not yet been batched for release. Typically the batching process occurs a few times per day.

Given this, it is not recommended to poll for status updates. Instead the recommended method is to register for the ACH.Payment.Sent webhook event, as we did in the steps above. The ACH.Payment.Sent webhook is fired once the status transitions to Processing, which indicates the payment has been successfully accepted by the Federal Reserve. The status will transition to Complete once the payment posts to the funding account. In the case of push payments, this happens almost immediately after Processing since we are sending money. However for pull payments, this transition could take up to a day and is driven by the effective date of the payment.

Here's an example of the Ach.Payment.Sent event for the payment we originated in this scenario:

{
  "id": "7c135bfa-234f-48d2-9d70-adc70135d15f",
  "eventName": "Ach.Payment.Sent",
  "status": "Pending",
  "partnerId": "30dee145-b6a2-4058-8dc3-ac4000dee91f",
  "createdAt": "2021-10-20T14:48:01.12-04:00",
  "resources": [
    "ach/v1/payments/475dbdae-93a6-40ab-9962-ad3b0130e4fc"
  ],
  "details": []
}

Receiving this event confirms the end of the outbound payment lifecycle. At this point there is no need to perform an additional query to determine the payment's status. The payment ID included in the event resources allows us to reconcile against the same payment ID provided by the response to the payment origination request.

❗️

Avoid Searching Payments for Reconciliation Purposes

The GET /ach/v1/payments and GET /ach/v1/payments/{id} endpoints must not be used as a method to reconcile payments you originate. Partners should incorporate webhooks into their payment reconciliation processes.

Handling Returns

Sometimes ACH payments get returned by the receiving bank. There are a variety of reasons why this can happen, as well as varying time frames. For example, the receiver account number specified cannot be found by the receiving bank. Most returns occur within 2 business days. For a full listing of return reasons click here.

When your payment is returned, a new payment record is created with a paymentType set to Return. The new payment record is correlated to the origination record by the original.paymentId field. It is important to understand that once a payment's status transitions to Complete, there will be no further updates to that payment record, even if the payment is returned.

Lastly, we are notified of the returned payment via the ACH.Return.Received webhook we registered in the steps above. At which point, you would query the payment resource specified in the event body to obtain the original payment ID and return reason code found in the reasonCode field.

Here's an example of an ACH return for the payment we originated earlier in this scenario:

{
  "id": "7c135bfa-234f-48d2-9d70-adc70135d15f",
  "eventName": "Ach.Return.Received",
  "status": "Pending",
  "partnerId": "30dee145-b6a2-4058-8dc3-ac4000dee91f",
  "createdAt": "2021-10-20T14:48:01.12-04:00",
  "resources": [
    "ach/v1/payments/4541e125-6664-4e60-bef7-adc70146e52e"
  ],
  "details": []
}

The event contains the payment ID which you'd use to query the details of the return payment. Here's an example of that request:

GET /ach/v1/payments/{id}
{
  "id": "4541e125-6664-4e60-bef7-adc70146e52e",
  "accountNumber": "300012341234",
  "referenceId": "A293E51YH9CP",
  "paymentType": "Return",
  "direction": "Inbound",
  "status": "Completed",
  "source": "File",
  "postingType": "Individual",
  "fedBatchId": "ebc50859-dcd2-43af-b075-adc70146e4d3",
  "fedBatchSequence": 1,
  "coreTransactionId": "34260a5f-8a48-4dda-b136-adc701472fa3",
  "postingCode": "OK",
  "posting": "Posted",
  "originator": {
    "routingNumber": "021214891",
    "accountType": "Checking",
    "name": "Acme Co",
    "identification": "9999913512",
    "data": ""
  },
  "receiver": {
    "routingNumber": "021000021",
    "accountNumber": "456789000",
    "accountType": "Checking",
    "name": "Bob Smith",
    "identification": "XYZ123"
  },
  "secCode": "WEB",
  "description": "Payment",
  "transactionType": "Push",
  "amount": 10000,
  "serviceType": "SameDay",
  "extendedDetails": {
    "paymentType": ""
  },
  "effectiveDate": "210603",
  "reasonCode": "R02",
  "reasonData": "",
  "traceNumber": "021200336631345",
  "settlementDate": "293",
  "wasReturned": false,
  "wasCorrected": false,
  "holdDays": 0,
  "previous": {
    "paymentId": "475dbdae-93a6-40ab-9962-ad3b0130e4fc",
    "traceNumber": "021214895167146"
  },
  "original": {
    "paymentId": "475dbdae-93a6-40ab-9962-ad3b0130e4fc",
    "traceNumber": "021214895167146"
  },
  "createdAt": "2021-10-20T15:50:14.03-04:00",
  "processedAt": "2021-10-20T15:51:13.57-04:00",
  "completedAt": "2021-10-20T15:51:15.977-04:00",
  "postedAt": "2021-10-20T15:51:15.977-04:00",
  "partnerId": "30dee145-b6a2-4058-8dc3-ac4000dee91f",
  "productId": "a5340639-a94f-4d03-9e80-ad20013faec4",
  "lastModifiedAt": "2021-10-20T15:51:16.0102224-04:00"
}

The example above displays R02 in the reason code field which means Account Closed. The previous payment ID displays the ID of the outbound payment we originated earlier in this scenario. You'll notice that the original payment ID also has this same information. The only time this will differ is in scenarios where payments are being dishonored or contested. For example, let's say you originated an outbound payment on a Monday and received an ACH return for that payment months later, but the type of return isn't allowed by NACHA rules so Cross River Bank dishonors the return. When CRB originates the dishonored return payment, its details will include the return in the previous payment ID field and the initial outbound payment in the original payment ID field.

Handling Notifications of Change (NOCs)

There are times where CRB will receive a NOC related to an outbound payment you previously originated. COS refers to NOCs as "corrections". Unlike an ACH return, a NOC indicates:

  • The payment you previously originated has posted to the receiver's account, and
  • The payment you previously originated contained an error

There are a variety of reasons why this can happen. For a full listing of correction codes click here. You can register for the Ach.Noc.Received webhook event to be notified of an inbound NOC.

Here's an example of a NOC webhook event for the payment we originated earlier in this scenario:

{
  "id": "7c135bfa-234f-48d2-9d70-adc70135d15f",
  "eventName": "Ach.Noc.Received",
  "status": "Pending",
  "partnerId": "30dee145-b6a2-4058-8dc3-ac4000dee91f",
  "createdAt": "2021-10-20T14:48:01.12-04:00",
  "resources": [
    "ach/v1/payments/66238867-1c03-44a7-a4ff-30aa768f1b08"
  ],
  "details": []
}

The event contains the payment ID which you'd use to query the details of the return payment. Here's an example of that request:

GET /ach/v1/payments/{id}
{
  "id": "66238867-1c03-44a7-a4ff-30aa768f1b08",
  "accountNumber": "300012341234",
  "referenceId": "A293E51YH9CP",
  "paymentType": "Correction",
  "direction": "Inbound",
  "status": "Completed",
  "source": "File",
  "postingType": "Individual",
  "fedBatchId": "94354eb8-470a-48af-8e37-b7d3f395ce2d",
  "fedBatchSequence": 1,
  "coreTransactionId": "24a6fb5c-ca0e-4667-8ad4-a86831fd3d1f",
  "postingCode": "OK",
  "posting": "Posted",
  "originator": {
    "routingNumber": "021214891",
    "accountType": "Checking",
    "name": "Acme Co",
    "identification": "9999913512",
    "data": ""
  },
  "receiver": {
    "routingNumber": "021000021",
    "accountNumber": "456789000",
    "accountType": "Checking",
    "name": "Bob Smith",
    "identification": "XYZ123"
  },
  "secCode": "WEB",
  "description": "Payment",
  "transactionType": "Push",
  "amount": 10000,
  "serviceType": "SameDay",
  "extendedDetails": {
    "paymentType": ""
  },
  "effectiveDate": "210603",
  "reasonCode": "C01",
  "reasonData": "4567890012",
  "traceNumber": "021200558622449",
  "settlementDate": "293",
  "wasReturned": false,
  "wasCorrected": false,
  "holdDays": 0,
  "previous": {
    "paymentId": "475dbdae-93a6-40ab-9962-ad3b0130e4fc",
    "traceNumber": "021214895167146"
  },
  "original": {
    "paymentId": "475dbdae-93a6-40ab-9962-ad3b0130e4fc",
    "traceNumber": "021214895167146"
  },
  "createdAt": "2021-10-20T15:50:14.03-04:00",
  "processedAt": "2021-10-20T15:51:13.57-04:00",
  "completedAt": "2021-10-20T15:51:15.977-04:00",
  "postedAt": "2021-10-20T15:51:15.977-04:00",
  "partnerId": "30dee145-b6a2-4058-8dc3-ac4000dee91f",
  "productId": "a5340639-a94f-4d03-9e80-ad20013faec4",
  "lastModifiedAt": "2021-10-20T15:51:16.0102224-04:00"
}

The example above displays C01 in the reason code field which means Incorrect DFI Account Number. The reason data field displays the correct account number for the receiver. which should have been 4567890012 and not 4567890000. When an NOC is received, you should update your internal records with the information you receive in the reason data field so that any subsequent payments are sent with the correct information.


Did this page help you?