Skip to main content

ACH Direct Debit

Please refer Server Side Implementation for details on existing Server Support.

ACH Direct Debit is a payment method that runs on the ACH (Automated Clearing House) network in the United States. It allows customers to pay for transactions by debiting directly from their bank account, as opposed to processing through a card brand.

At a high level, accepting ACH payments consists of four concepts:

  • Tokenizing
  • Vaulting
  • Verifying
  • Transacting

Depending on how you wish to verify your customer's bank account, these concepts will overlap in different ways.

Tokenizing

Tokenizing is the process of exchanging raw payment information for a secure, single-use token called a payment method.

Tokenizing without verification

You can tokenize a bank account by collecting a customer's bank details, including account and routing numbers. However, this will not verify the bank account, and the resulting payment method will not be transactable. Before you can create a transaction, you will need to use the payment method to vault the bank account and successfully verify it.

To tokenize an US bank account, you should use tokenizeUsBankAccount mutation.

For input you would provide the bank account details in usBankAccount.

mutation

mutation TokenizeUsBankAccount($input: TokenizeUsBankAccountInput!) {
tokenizeUsBankAccount(input: $input) {
clientMutationId
paymentMethod {
id
usage
createdAt
details {
... on UsBankAccountDetails {
accountholderName
accountType
bankName
last4
routingNumber
verified
achMandate {
acceptedAt
acceptanceText
}
}
}
}
}
}

variables

{
"input": {
"usBankAccount": {
"routingNumber": "021000021",
"accountNumber": "9876543210",
"accountType": "CHECKING",
"achMandate": "I agree to give away all my money",
"individualOwner": {
"firstName": "Busy",
"lastName": "Bee"
},
"billingAddress": {
"streetAddress": "111 Main St",
"extendedAddress": "#7",
"city": "San Jose",
"state": "CA",
"zipCode": "94085"
}
}
}
}

response

{
"data": {
"tokenizeUsBankAccount": {
"clientMutationId": null,
"paymentMethod": {
"id": "tokenusbankacct_rest_of_token",
"usage": "SINGLE_USE",
"createdAt": "2022-08-10T23:19:17.117000Z",
"details": {
"accountholderName": "Busy Bee",
"accountType": "CHECKING",
"bankName": "JPMORGAN CHASE",
"last4": "3210",
"routingNumber": "021000021",
"verified": false
}
}
}
},
"extensions": {
"requestId": "cfcc38fe-6e93-4fcf-aa1a-a62e2f37c676"
}
}

Note: The resulting token that represents the bank account can be found in data.tokenizeUsBankAccount.paymentMethod.id. In example above, it's the string with "tokenusbankacct_" prefix.

Vaulting

Vaulting is the process of exchanging a single-use payment method ID for a multi-use payment method ID. For our ACH integration, vaulting includes verification options:

Vaulting with verification

When vaulting a bank account, you can choose to specify a verification method. This creates a vaulted payment method and initiates a verification in a single step. If the verification fails, you can keep retrying verifications on the same payment method. A vaulted payment method will only be transactable once it has had a successful verification.

To vault an ACH payment method, you should use vaultUsBankAccount mutation.

For input you would provide at least a single-use ACH paymentMethod. Optionally, you can also provide a verificationMethod which specifies the way in which you want the given bank account verified.

mutation

mutation VaultUsBankAccount($input: VaultUsBankAccountInput!) {
vaultUsBankAccount(input: $input) {
clientMutationId
paymentMethod {
id
legacyId
details {
... on UsBankAccountDetails {
accountholderName
accountType
bankName
last4
routingNumber
verified
achMandate {
acceptedAt
acceptanceText
}
}
}
}
verification {
id
status
}
}
}

variables

{
"input": {
"paymentMethodId": "tokenusbankacct_bf_4t32qt_b2p7v9_x7gcpk_q7rjvf_2n3",
"verificationMerchantAccountId": "paypal",
"verificationMethod": "MICRO_TRANSFERS"
}
}

response

{
"data": {
"vaultUsBankAccount": {
"clientMutationId": null,
"paymentMethod": {
"id": "cGF5bWVudG1ldGhvZF91c2JfYnQ4a3BrNg",
"legacyId": "bt8kpk6",
"details": {
"accountholderName": "Busy Bee",
"accountType": "CHECKING",
"bankName": "JPMORGAN CHASE",
"last4": "3210",
"routingNumber": "021000021",
"verified": false,
"achMandate": {
"acceptedAt": "2022-08-11T06:26:34.000000Z",
"acceptanceText": "I agree to give away all my money today."
}
}
},
"verification": {
"id": "dXNiYW5rYWNjb3VudHZlcmlmaWNhdGlvbl85Y3F2M3QzOA",
"status": "PENDING"
}
}
},
"extensions": {
"requestId": "126e6b6f-3205-42c7-b4b7-844c42b333f2"
}
}

Vaulting without verification

You can also vault a bank account without specifying a verification method. This will vault the payment method as usual, but no verification will be attempted. As stated above, this payment method will not be transactable until it has a subsequent successful verification. If you prefer to perform verification on your own, the verificationMethod should be set as INDEPENDENT_CHECK.

Verifying

Braintree’s ACH Direct Debit integration offers several methods for verifying that the customer owns the bank account they provide to you for payment:

  • Network check – instantly verifies the bank account details using bank account and routing number, as well as personal/business information
  • Micro-transfers – issues two separate credits of less than a dollar each to the customer’s bank account and requires the customer to confirm the exact amounts once they’re visible in the account
  • Independent check – allows you to use your own verification method that’s not listed above and manually mark payment methods as verified

You can use any combination of these methods in order to meet your business needs. For example, one recommended flow is to use network check as an initial verification method with micro-transfers as a backup option.

Verifying with network check

To verify an US bank account, you should use verifyUsBankAccount mutation.

For input you would provide at the minimum paymentMethodId and verificationMethod.

mutation

mutation VerifyUsBankAccount($input: VerifyUsBankAccountInput!) {
verifyUsBankAccount(input: $input) {
clientMutationId
verification {
id
legacyId
status
merchantAccountId
createdAt
gatewayRejectionReason
paymentMethod {
id
}
processorResponse {
legacyCode
message
cvvResponse
avsPostalCodeResponse
avsStreetAddressResponse
}
}
}
}

variables

{
"input": {
"paymentMethodId": "cGF5bWVudG1ldGhvZF91c2JfNGt4ZjI5Ng",
"verificationMethod": "INDEPENDENT_CHECK"
}
}

response

{
"data": {
"verifyUsBankAccount": {
"clientMutationId": null,
"verification": {
"id": "dXNiYW5rYWNjb3VudHZlcmlmaWNhdGlvbl9qeTZ2Zjg5eA",
"legacyId": "jy6vf89x",
"status": "VERIFIED",
"merchantAccountId": "paypal",
"createdAt": "2022-08-11T05:06:10.000000Z",
"gatewayRejectionReason": null,
"paymentMethod": {
"id": "cGF5bWVudG1ldGhvZF91c2JfNGt4ZjI5Ng"
},
"processorResponse": {
"legacyCode": "1000",
"message": "Approved",
"cvvResponse": null,
"avsPostalCodeResponse": null,
"avsStreetAddressResponse": null
}
}
}
},
"extensions": {
"requestId": "c33bf5ea-e978-4c03-82d2-1a13067f9a78"
}
}

To check for successful verification, you should examine verifyUsBankAccount.verification.processorResponse.message and look for "Approved". If verification has failed, some other message is returned, such as "Processor Network Unavailable - Try Again".

Verifying with Micro-deposit

To verify an US bank account using micro-deposit random, small amounts of your choice, you should first use confirmMicroTransferAmounts mutation to do the first step.

For input you would provide at the minimum verificationId which is vaultUsBankAccount.verification.id (in the response of vaultUsBankAccount mutation) and amountInCents which is an array of integers. For instance, to specify 17 cents and 44 cents, you would provide [17, 44].

mutation

mutation ConfirmMicroTransferAmounts($input: ConfirmMicroTransferAmountsInput!) {
confirmMicroTransferAmounts(input: $input) {
verification {
id
paymentMethod {
id
usage
}
merchantAccountId
status
processorResponse {
message
cvvResponse
avsPostalCodeResponse
avsStreetAddressResponse
}
networkResponse {
code
message
}
gatewayRejectionReason
}
status
}
}

variables

{
"input": {
"verificationId": "dXNiYW5rYWNjb3VudHZlcmlmaWNhdGlvbl85Y3F2M3QzOA",
"amountsInCents": [17, 44]
}
}

response

{
"data": {
"confirmMicroTransferAmounts": {
"verification": {
"id": "dXNiYW5rYWNjb3VudHZlcmlmaWNhdGlvbl85Y3F2M3QzOA",
"paymentMethod": {
"id": "cGF5bWVudG1ldGhvZF91c2JfYnQ4a3BrNg",
"usage": "MULTI_USE"
},
"merchantAccountId": "paypal",
"status": "PENDING",
"processorResponse": {
"message": "Approved",
"cvvResponse": null,
"avsPostalCodeResponse": null,
"avsStreetAddressResponse": null
},
"networkResponse": {
"code": null,
"message": null
},
"gatewayRejectionReason": null
},
"status": "AMOUNTS_DO_NOT_MATCH"
}
},
"extensions": {
"requestId": "6366e6c2-192c-4bac-b87f-3149eecec7b7"
}
}

Checking verification status

To check for successful verification, you should examine verifyUsBankAccount.verification.processorResponse.message and look for "Approved". If verification has failed, some other message is returned, such as "Processor Network Unavailable - Try Again".

Retrying verification

Sometimes verification would fail. You can retry verification by sending the the previous request again or use the previous request but change the verificationMethod. Say, previously, NETWORKCHECK was used and failed with message "_No Data Found - Try Another Verification Method", on a retry, you should change it to MICRO_TRANSFERS as an alternative.

Common errors

Note that each tokenized US bank account can only be vaulted once. If you attempt to vault the same token more than once, you will get the following error.

errors

{
"errors": [
{
"message": "Cannot use a single-use payment method more than once.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"vaultUsBankAccount"
],
"extensions": {
"errorClass": "VALIDATION",
"errorType": "user_error",
"inputPath": [
"input",
"paymentMethodId"
],
"legacyCode": "93107"
}
}
],
"data": {
"vaultUsBankAccount": null
},
"extensions": {
"requestId": "ba53c8e6-43b2-464a-b0f8-bc1599b96af7"
}
}

If you do not provide verificationMethod in the inputs, you will get the following error.

errors

{
"errors": [
{
"message": "Variable 'input' has an invalid value: Field 'verificationMethod' has coerced Null value for NonNull type 'UsBankAccountVerificationMethod!'",
"locations": [
{
"line": 1,
"column": 29
}
]
}
],
"extensions": {
"requestId": "7fd3e425-ff70-43bc-8cb5-9014fdedccff"
}
}