Originating ACH Payments
In this tutorial you will learn how to
Originate an ACH push payment.
Register the relevant webhooks to receive status updates on your payment.
Handle rejected and returned payments.
Handle Notifications of Change (NOC).
Before starting the tutorial, please make sure you have:
- API credentials
- Partner ID
- Master Account Number
The Cross River system is designed to authorize a transaction for each payment request as part of the payment lifecycle. For example, when an outbound push payment is created, a debit memo post is automatically placed on the originator account to secure funds before the payment is released to the Federal Reserve. This greatly reduces risk of loss to you and Cross River.
All payments have a related core transaction. The payment status is directly impacted by the posting status of the core transaction. This means that a payment cannot progress to Complete
until its related core transaction also has a status of Complete.
Get an access token
Before calling the COS API you must have API credentials including an access token. We recommend you cache the access token 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.
Register webhooks
Webhooks allow you to receive notification when certain events occur within the system. Webhooks report to your system via real-time notifications and eliminate the need to poll the API to follow account changes and updates.
For more information about webhook registrations click here.
After you originate the ACH payment, you need to know when it is accepted by the Federal Reserve.
➤ To register for the relevant webhook, call the webhook registration endpoint. The eventName
is ACH.Payment.Sent.
POST /webhooks/v1/registrations
{
"partnerId": "00000000-0000-0000-0000-000000000000",
"eventName": "ACH.Payment.Sent",
"registrationType": "Push",
"callbackUrl": "https://cos.yourcompanysite.com/ach-events"
}
➤ To check if the payment was returned by the receiving bank register for the ACH.Return.Received webhook.
POST /webhooks/v1/registrations
{
"partnerId": "00000000-0000-0000-0000-000000000000",
"eventName": "ACH.Return.Received",
"registrationType": "Push",
"callbackUrl": "https://cos.yourcompanysite.com/ach-events"
}
For a full listing of available ACH webhook events click here.
Note
The initial events you receive will have a status of Pending in the event object. When an event is created and pushed to your callback URL, the system considers any 200-type response it receives as a successful delivery. This updates the event status to Success, however, you will not receive a subsequent event with this updated status.
Originate the payment
There are two types of ACH payments.
- Push payment
When you transfer money to an account in another bank. - Pull payment
When you request funds from an account in another bank.
In this tutorial, let's send $100 to Bob Smith who has an account at another bank. Since we are transferring the money to Bob's account the paymentType
attribute must be Push.
To originate the payment
Call POST /ach/v1/payments
.
The details of the push payment are defined by the call attributes.
accountNumber | The account funding the payment |
receiver object | Details on the bank and account receiving the payment. |
routingNumber | Used by the Federal Reserve to route the payment instruction to the appropriate receiving bank. |
accountNumber | The account number in the receiving bank. |
name | The name associated with the account number in the receiving bank. |
identification | A reference value that may 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. |
amount | The amount of funds being transferred. The amount is in cents. |
serviceType | The ACH service type. Either SameDay or Standard. Note that SameDay is only supported on domestic payments up to $1,000,000. |
Payment request
POST /ach/v1/payments
{
"accountNumber": "2714035231",
"receiver": {
"routingNumber": "021000021",
"accountNumber": "456789000",
"accountType": "Checking",
"name": "Bob Smith",
"identification": "XYZ123",
},
"secCode": "WEB",
"description": "Payment",
"transactionType": "Push",
"amount": 10000,
"serviceType": "SameDay",
"clientIdentifier": "21fe77da-e2f8-4475-9397-81a293d63b8x"
}
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.
Successful payment response
A successful JSON response returns the details of your originated payment:
{
"id": "b96b935a-4713-4aae-973b-aeee00f1a749",
"accountNumber": "2714035231",
"referenceId": "A2236S8S20FH",
"paymentType": "Origination",
"direction": "Outbound",
"status": "Created",
"source": "Api",
"postingType": "Individual",
"postingCode": "OK",
"posting": "Pending",
"originator": {
"routingNumber": "021214891",
"accountNumber": "2714035231",
"accountType": "Checking",
"name": "Cross River Bank",
"identification": "021214891"
},
"receiver": {
"routingNumber": "021000021",
"accountNumber": "456789000",
"accountType": "Checking",
"name": "Bob Smith",
"identification": "XYZ123"
},
"secCode": "WEB",
"description": "Payment",
"transactionType": "Push",
"amount": 10000,
"serviceType": "SameDay",
"effectiveDate": "220811",
"traceNumber": "021214898943562",
"wasReturned": false,
"wasCorrected": false,
"holdDays": 0,
"original": {
"paymentId": "b96b935a-4713-4aae-973b-aeee00f1a749"
},
"createdAt": "2022-08-11T10:39:49.9996701-04:00",
"partnerId": "1e5d3f04-ae24-4af6-9e30-aecf012b99dd",
"productId": "cc62e17f-5912-483e-9e42-aed30112fbb6",
"lastModifiedAt": "2022-08-11T10:39:49.9996701-04:00",
"clientIdentifier": "21fe77da-e2f8-4475-9397-81a293d63b8x"
}
The status
attribute in response indicates that your payment was created. It does not indicate that your payment was successful. Once your payment is created, you can perform additional payment validation.
For example
You originate a $100 push payment from your account but your account only had a balance of $5. The payment will be reject due to insufficient funds. You won't be informed of this the API response body. You are alerted to this issue via the
Ach.Payment.Rejected
webhook event.
Payment confirmation
After originating the payment, it will typically remain in a Pending or Hold status for up to several hours, after which it will be in Created status. If the payment is originated close to a weekend or bank holiday, it may take a few days for the status to transition to Created
- A Pending status lets you know that the payment request is created but has not been batched for release. The batching process occurs a few times per day.
- A Hold status indicates that the payment request is in review and has not yet been approved for release to the Federal Reserve.
To receive updates on the status of the payment you need to register for the ACH.Payment.Sent webhook event.
The webhook is sent when the payment status transitions to Processing. This indicates that the payment was successfully accepted by the Federal Reserve.
The status will be Complete when the payment posts in the receiving account.
Do not poll for status updates or reconciliation purposes.
Best practice is to incorporate webhooks into the payment reconciliation processes.
TheGET /ach/v1/payments
andGET /ach/v1/payments/{id}
endpoints must not be used as a method to reconcile payments you originate.
➤ Here's an example of the Ach.Payment.Sent
event for the payment we originated in our tutorial.
{
"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": []
}
This event confirms the end of the outbound payment lifecycle. At this point you don't need to make any additional query to determine the payment status. The payment status updates to Complete
when the transaction is successfully posted.
The payment ID included in the resources array of the Ach.Payment.Sent
event allows us to reconcile against the same payment ID provided by the response to the payment origination request.
Handling rejected payments
There may be situations where your payment request is rejected after receiving a Success
response from POST /ach/v1/payments
. This can occur as a result of:
-
The transaction was unable to post from the account being used for your request.
For example, you originated a push payment that was higher than the balance of the originating account. Another example, you originate a payment from an account that has an active restriction placed on it.
In this situation, the payment details contain the reason the payment was rejected. -
The payment was rejected by our Cross River Operations or BSA/AML team.
This type of rejection is manual and the payment details do not contain the reason the payment was rejected. In these cases you need to contact Cross River for additional information.
Our tutorial simulates a rejection for technical reasons. To simulate the rejection, we have to originate a push payment for an amount that exceeds the originating account balance.
Again, let's send $100 to Bob Smith who has an account at another bank.
- Call
POST /ach/v1/payments
. - Define the call attributes like you did in the Originating payments section.
{
"accountNumber": "2674958042",
"receiver": {
"routingNumber": "021000021",
"accountNumber": "456789000",
"accountType": "Checking",
"name": "Bob Smith",
"identification": "XYZ123",
},
"secCode": "WEB",
"description": "Payment",
"transactionType": "Push",
"amount": 10000,
"serviceType": "SameDay",
"clientIdentifier": "bd3e0315-5f29-4b1c-a004-1fcdd0b8bee1"
}
The following response is returned.
{
"id": "f868a125-22b8-4c7e-a8dd-aeee00f76ce7",
"accountNumber": "2674958042",
"referenceId": "A223O84JDV79",
"paymentType": "Origination",
"direction": "Outbound",
"status": "Created",
"source": "Api",
"postingType": "Individual",
"postingCode": "OK",
"posting": "Pending",
"originator": {
"routingNumber": "021214891",
"accountNumber": "2674958042",
"accountType": "Checking",
"name": "Cross River Bank",
"identification": "021214891"
},
"receiver": {
"routingNumber": "021000021",
"accountNumber": "456789000",
"accountType": "Checking",
"name": "Bob Smith",
"identification": "XYZ123"
},
"secCode": "WEB",
"description": "Payment",
"transactionType": "Push",
"amount": 10000,
"serviceType": "SameDay",
"effectiveDate": "220811",
"traceNumber": "021214896697194",
"wasReturned": false,
"wasCorrected": false,
"holdDays": 0,
"original": {
"paymentId": "f868a125-22b8-4c7e-a8dd-aeee00f76ce7"
},
"createdAt": "2022-08-11T11:00:50.8991331-04:00",
"partnerId": "1e5d3f04-ae24-4af6-9e30-aecf012b99dd",
"productId": "d5f3ce06-8550-413e-a2e1-aed1016d6eea",
"lastModifiedAt": "2022-08-11T11:00:50.8991331-04:00",
"clientIdentifier": "bd3e0315-5f29-4b1c-a004-1fcdd0b8bee1"
}
➤ The Ach.Payment.Rejected
webhook event is triggered.
The resources
array in the event object contains the payment ID from the response body above.
{
"id": "496659ff-6034-480a-84af-aeee00f78ade",
"eventName": "Ach.Payment.Rejected",
"status": "Pending",
"partnerId": "1e5d3f04-ae24-4af6-9e30-aecf012b99dd",
"createdAt": "2022-08-11T11:01:16.483-04:00",
"resources": [
"ach/v1/payments/f868a125-22b8-4c7e-a8dd-aeee00f76ce7"
],
"details": []
}
➤ To query the reason the payment was rejected, call GET /ach/v1/payments/{id}
where {id} is the payment ID from the webhook event resources
object.
- The rejection reason is found in the
postingCode
attribute.
{
"id": "f868a125-22b8-4c7e-a8dd-aeee00f76ce7",
"accountNumber": "2674958042",
"referenceId": "A223O84JDV79",
"paymentType": "Origination",
"direction": "Outbound",
"status": "Rejected",
"source": "Api",
"postingType": "Individual",
"memoPostId": "e9f4fa59-2901-40fd-8c90-aeee00f76f7d",
"postingCode": "NSF",
"posting": "Failed",
"originator": {
"routingNumber": "021214891",
"accountNumber": "2674958042",
"accountType": "Checking",
"name": "Cross River Bank",
"identification": "021214891"
},
"receiver": {
"routingNumber": "021000021",
"accountNumber": "456789000",
"accountType": "Checking",
"name": "Bob Smith",
"identification": "XYZ123"
},
"secCode": "WEB",
"description": "Payment",
"transactionType": "Push",
"amount": 10000,
"serviceType": "SameDay",
"effectiveDate": "220811",
"traceNumber": "021214896697194",
"wasReturned": false,
"wasCorrected": false,
"holdDays": 0,
"original": {
"paymentId": "f868a125-22b8-4c7e-a8dd-aeee00f76ce7"
},
"createdAt": "2022-08-11T11:00:50.9-04:00",
"rejectedAt": "2022-08-11T11:00:51.297-04:00",
"partnerId": "1e5d3f04-ae24-4af6-9e30-aecf012b99dd",
"productId": "d5f3ce06-8550-413e-a2e1-aed1016d6eea",
"lastModifiedAt": "2022-08-11T11:00:51.2980853-04:00",
"clientIdentifier": "bd3e0315-5f29-4b1c-a004-1fcdd0b8bee1"
}
Handling Returns
Sometimes ACH payments are returned by the receiving bank. There are several reasons this may occur. You can find a full list of return reasons here. Most returns occur within 2 business days, but may take longer.
For example, the receiver account number specified cannot be found by the receiving bank.
When a payment is returned, a new payment record is created with a paymentType
set as Return the new payment record is connected to the original payment record with the previous.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 webhook event:
{
"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 includes the payment ID used to query the details of the return payment. It lets you determine which outbound payment the return is associated with. When we query the payment details the following information is returned:
GET /ach/v1/payments/{id}
{
"id": "4541e125-6664-4e60-bef7-adc70146e52e",
"accountNumber": "300012341234",
"referenceId": "A293E51YH9CP",
"paymentType": "Return",
"direction": "Inbound",
"status": "Complete",
"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 response returns reasonCode
of R02. This means Account Closed. The previous payment ID field shows the ID of the outbound payment we originated earlier in this scenario.
☛ Notice that the original payment ID has the same information. The only time the information will be different is in scenarios where payments are being dishonored or contested.
For example
Let's say you originated an outbound payment on Monday and received an ACH return for that payment months later. That type of return isn't allowed by Nacha rules and so Cross River Bank dishonors the return. When Cross River originates the dishonored return payment its details 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 situations when Cross River receives an ACH notification of change (NOC) related to an outbound payment you originated. When you create an API call in COS a NOC is referred to a Correction in the paymentType
attribute. Unlike an ACH return, a NOC indicates:
- The payment you previously originated has posted to the receiver account
- The payment you previously originated contained an error
There are several reasons why this can happen. A full listing of correction codes can be found 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 tutorial:
{
"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 you use to query the details of the return payment.
GET /ach/v1/payments/{id}
{
"id": "66238867-1c03-44a7-a4ff-30aa768f1b08",
"accountNumber": "300012341234",
"referenceId": "A293E51YH9CP",
"paymentType": "Correction",
"direction": "Inbound",
"status": "Complete",
"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 reasonCode
attribute in the call is set to C01. This indicates an Incorrect DFI Account Number. The reasonData
attribute is the correct receiver account number that should have been *4567890012* and not 4567890000. When a NOC is received, update your internal records with the information you receive in the reasonData
attribute so subsequent payments are sent with the correct information.
Updated 25 days ago