Accept card payments

With Moov, you can build a flow for accepting card payments. Some examples of when card acceptance is useful:

  • An e-commerce platform that allows buyers and sellers to transact
  • A software-as-a-service (SaaS) platform connecting service providers with clients
  • A utility or subscription based company looking to allow their customers different payment options

Below, we provide instructions on how to implement card acceptance with Moov. The examples provided apply to both a merchant taking payments from customers through a checkout flow, or a platform collecting card payments payments directly from a customer.

This guide covers using Moov Drops or Moov.js to onboard accounts and link cards. If you’re just getting started, refer to our guide on how to install and authenticate Moov.js.

Onboard the merchant

With Moov, all transfers represent the movement of money from a source to a destination. In the card acceptance scenario, the source is a cardholder and the destination can be you or a merchant on your platform.

You and/or your merchants’ accounts will need the collect-funds capability as shown below. These accounts will need to be verified and undergo underwriting.

Use the API directly via each of the following curl commands, or use the Moov Dashboard to complete the following steps:

Create the account

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
curl -X POST "https://api.moov.io/accounts" \
  -H "Authorization: Bearer {token}" \
  --data-raw '{
    "accountType": "business",
    "profile": {
      "business": {
        "legalBusinessName": "Whole Body Fitness LLC",
        "businessType": "llc"
      }
    },
    "capabilities": [
      "transfers", 
      "collect-funds"
    ]
  }'\

Get the TOS token

1
2
curl -X GET "https://api.moov.io/tos-token" \
  -H "Authorization: Bearer {token}" \

Patch the TOS token

1
2
3
4
5
6
7
curl -X PATCH "https://api.moov.io/accounts/{merchant-account-ID}" \
  -H "Authorization: Bearer {token}" \
  --data-raw ' {
    "termsOfService": {
      "token": "tos-token-string" 
    }
  }'\

Add representatives

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
curl -X POST "https://api.moov.io/accounts/{merchant-account-ID}/representatives" \
  -H "Authorization: Bearer {token}" \
  --data-raw '{
    "name": {
      "firstName": "Amanda",
      "lastName": "Yang"
    },
    "address": {
      "addressLine1": "12 Main Street",
      "city": "Cabot Cove",
      "stateOrProvince": "ME",
      "postalCode": "04103",
      "country": "US"
    },
    "birthDate": {
      "day": 10,
      "month": 11,
      "year": 1985
    },
    "email": "amanda@classbooker.dev",
    "governmentID": { 
      "ssn": {
        "full": "111111111"
      }
    },
    "responsibilities": { 
      "isController": true,
      "isOwner": true,
      "ownershipPercentage": 38,
      "jobTitle": "CEO"
    }
  }'\

Provide MCC and business description for underwriting

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
curl -X PATCH "https://api.moov.io/accounts/{accountID}" \
  -H "Authorization: Bearer {token}" \
  --data-raw '{
    "profile": {
      "business": {
        "description": "some-description",
        "mcc": "string"
      }
    }
  }'\

Provide transaction data for underwriting

1
2
3
4
5
6
7
curl -X PUT "https://api.moov.io/underwriting" \
  -H "Authorization: Bearer {token}" \
  --data-raw '{
    "averageTransactionSize": 10000,
    "maxTransactionSize": 50000,
    "averageMonthlyTransactionVolume": 250000
  }'\
Depending on the type of business you are onboarding, you may need to update the ownersProvided field in the accounts PATCH endpoint.

Alternative onboarding methods

Alternatively, you can onboard using the Node SDK, Moov.js with your own UI, or Moov Drops (with a pre-built UI for onboarding).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// create Moov instance with generated token
const moov = new Moov(credentialsObject);

// create Moov account, likely on form submit
moov.accounts.create({
  accountType: "business",
  profile: {business: {}},
  capabilities: ["transfers", "collect-funds"]
}).then((account) => {
  console.log(account);
}).catch((err) => {
  console.error(err);
});

// Generate a new token server-side with the accountID and update the token within your frontend app. You can chain methods together
moov.setToken("new-token");

// add a representative to account
moov.accounts.representatives.create({ 
  accountID: "newly-created-accountID",
  representative: {}
});

// link a bank account
moov.accounts.bankAccounts.link({
  accountID: "newly-created-accountID",
  bankAccount: {
    holderName: "Name",
    holderType: "business",
    accountNumber: "0004321567000",
    routingNumber: "123456789",
    bankAccountType: "checking"
  }
}).then((bankAccount) => {
// kick off micro-deposit verification
  moov.accounts.bankAccounts.startMicroDepositVerification(accountID, bankAccountID);
});

// Later, verify micro-deposits
moov.accounts.bankAccounts.completeMicroDepositVerification(accountID, bankAccountID, [12, 45]);

// Asynchronously, the ach-debit-fund payment method will be created from this verified bank account. If you want to move funds out of the Moov wallet,  this bank account can be the destination of a wallet-to-bank transfer.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// create Moov instance with generated token
const moov = Moov(token);

// create Moov account, likely on form submit
moov.accounts.create({
  accountType: "business",
  profile: {business: {}},
  capabilities: ["transfers", "collect-funds"]
}).then((account) => {
  console.log(account);
}).catch((err) => {
  console.error(err);
});

// Generate a new token server-side with the accountID and update the token within your frontend app. You can chain methods together
moov.setToken("new-token");

// add a representative to account
moov.accounts.representatives.create({ 
  accountID: "newly-created-accountID",
  representative: {}
});

// link a bank account
moov.accounts.bankAccounts.link({
  accountID: "newly-created-accountID",
  bankAccount: {
    holderName: "Name",
    holderType: "business",
    accountNumber: "0004321567000",
    routingNumber: "123456789",
    bankAccountType: "checking"
  }
}).then((bankAccount) => {
// kick off micro-deposit verification
  moov.accounts.bankAccounts.startMicroDepositVerification({accountID, bankAccountID});
});

// Later, verify micro-deposits
moov.accounts.bankAccounts.completeMicroDepositVerification({accountID, bankAccountID, [12, 45]});

// Asynchronously, the ach-debit-fund payment method will be created from this verified bank account. If you want to move funds out of the Moov wallet,  this bank account can be the destination of a wallet-to-bank transfer.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Onboarding Moov Drop

const onboarding = document.querySelector("moov-onboarding");
// After generating a token, set it on the onboarding element
onboarding.token = "some-generated-token";

// Include your own accountID which can be found in the Moov Dashboard
onboarding.facilitatorAccountID = "your-account-id";

// Transfers and collect-funds capabilities are needed for this flow
onboarding.capabilities = ["transfers", "collect-funds"];

// Funding will occur with a bank account
onboarding.paymentMethodTypes = ["bankAccount"];

// Verify bank account with micro-deposits
onboarding.microDeposits = true;
// Follow the Onboarding Moov Drops and Plaid guides if linking bank accounts using Plaid

// Open the onboarding flow when ready
onboarding.open = true;

Onboard cardholders

Create a Moov account for the cardholder

You can create cardholder accounts by providing their full name and a valid phone number or email address. In a checkout scenario, you can create these accounts when the customer is prompted to pay. When you create accounts for your cardholders, they will automatically receive the transfers capability.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
curl -X POST "https://api.moov.io/accounts" \
  -H "Authorization: Bearer {token}" \
  --data-raw '{
    "accountType": "individual",
    "profile": {
      "individual": {
        "name": {
          "firstName": "Jules",
          "lastName": "Jackson"
        },
        "email": "julesjacksonyoga@moov.io"
      }
    },
    "foreignId": "your-correlation-id"
  }'\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const moov = new Moov(credentialsObject);

const accountPayload = {
  accountType: "individual",
  profile: {
    individual: {
      name: {
        firstName: "Jules",
        lastName: "Jackson"
      },
      phone: "1234567789",
      email: "julesjacksonyoga@moov.io"
    }
  },
  foreignId: "your-correlation-id"
};

const account = await moov.accounts.create(accountPayload);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const moov = Moov(token);

const accountPayload = {
  accountType: "individual",
  profile: {
    individual: {
      name: {
        firstName: "Jules",
        lastName: "Jackson"
      },
      phone: "1234567789",
      email: "julesjacksonyoga@moov.io"
    }
  },
  foreignId: "your-correlation-id"
};

const account = await moov.accounts.create({accountPayload});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Onboarding Moov Drop

const onboarding = document.querySelector("moov-onboarding");
// After generating a token, set it on the onboarding element
onboarding.token = "some-generated-token";

// Include your own accountID which can be found in the Moov Dashboard
onboarding.facilitatorAccountID = "your-account-id";

// Transfers capability needed for this flow
onboarding.capabilities = ["transfers"];

// Funding will occur with a card
onboarding.paymentMethodTypes = ["card"];

// Open the onboarding flow when ready
onboarding.open = true;

You can use the pre-built card link Drop in an HTML document as shown below. Once the information has been submitted via the Drop, Moov verifies the card details with card networks. You will not be responsible for storing or handling any of the card data, since all PII goes directly to Moov.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<moov-card-link class="card-link"></moov-card-link>

<script>
  let moovCardLink = document.querySelector("moov-card-link");
  moovCardLink.accountID = "accountID";
  moovCardLink.oauthToken = "oauthToken";
  moovCardLink.onSuccess = (card) => {
    console.log("Card linked");
    // now show a success screen
  }
  moovCardLink.onError = (error) => {
    console.log("Error linking card");
    // now show an error screen
  }

  // call submit when button is pressed
  moovCardLink.submit();
</script>

If you have submitted an attestation of PCI compliance to Moov, you also have the option to link cards directly through the API. If you opt for this method, you will be responsible for storing and handling card data.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
curl -X POST "https://api.moov.io/accounts/{accountID}/cards" \
  -H "Authorization: Bearer {token}" \
  -H "X-Wait-For: rail-response" \
  --data-raw '{
    "billingAddress": {
      "addressLine1": "123 Main Street",
      "city": "Denver",
      "country": "US",
      "postalCode": "80301",
      "stateOrProvince": "CO"
    },
    "cardCvv": "123",
    "cardNumber": "4111111111111111",
    "expiration": {
      "month": "01",
      "year": "28"
    },
    "holderName": "Jules Jackson"
  }'\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const moov = new Moov(credentialsObject);

const accountID = "accountID";
const cardPayload = {
  billingAddress: {
    addressLine1: "123 Main Street",
    city: "Denver",
    country: "US",
    postalCode: "80301",
    stateOrProvince: "CO"
  },
  cardCvv: "123",
  cardNumber: "4111111111111111",
  expiration: {
    month: "01",
    year: "27",
  },
  holderName: "Jules Jackson"
};

const card = await moov.cards.link(accountID, cardPayload);

Submit the card payment

The customer’s card will be the source of funds, so we’ll need the card-payment payment method from their account. You can obtain the paymentmethodID from the payment methods GET endpoint. The sourceID should be the cardID from the cards GETendpoint.

Since the merchant’s wallet will be the destination of the funds, we will need the moov-wallet payment method. You can obtain the paymentmethodID using the payment methods GET endpoint and finding the moov-wallet payment method associated with the merchant account.

To submit the card payment, create a transfer from the cardholder (source) to the merchant (destination).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
curl -X POST "https://api.moov.io/transfers" \
  -H "Authorization: Bearer {token}" \
  -H "X-Idempotency-Key: UUID" \
  -H "X-Wait-For: rail-response" \
  --data-raw '{
    "source": {
		// Cardholder - paymentMethodType = card-payment
      "paymentMethodID": "UUID"
    },
    "destination": {
		// Merchant - paymentMethodType = moov-wallet
      "paymentMethodID": "UUID"
    },
    "amount": {
      "value": 3000, // $30.00
      "currency": "USD"
    },
    "description": "Card payment example"
  }'\
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Authenticate
import { Moov } from '@moovio/node';

const moov = new Moov({
  accountID: "YOUR-MOOV-ACCOUNT-ID",
  publicKey: "PUBLIC-KEY",
  secretKey: "PRIVATE-KEY",
  domain: "YOUR-DOMAIN"
});

// Server side Node SDK example 
try {
  const transfer = {
    source: { paymentMethodID: "customer's card-payment payment method" },
    destination: { paymentMethodID: "merchant's moov-wallet payment method" },
    amount: {
      value: 3000, // $30.00
      currency: "USD"
    },
    description: "Card payment example"
  };
  const { transferID } = moov.transfers.create(transfer);
} catch (err) {
  // ...
}

Card-based payments first settle in a Moov wallet. The funds can later be moved to an external bank account through a disbursement transfer.

When you include the X-Wait-For header in the request, you will get a synchronous response that includes full transfer and rail-specific details from the payment network. This is useful for when a user is clicking a button to make a payment and needs a synchronous response so they can get confirmation that it worked. If you omit the X-Wait-For header, you will get an asynchronous response that only includes the transferID and the timestamp for when the transfer was created.

What’s next

If you receive a 202 response from creating a transfer, do not consider the transfer as successful or failed. Instead, you should wait for the webhook notification to determine the status of the transfer, or subsequently look up the transfer status using the transferID. Read more on our transfer responses guide.

After submitting the card payment, you may also want to:

If you’re interested in other use cases, you can dive into our guides:

Summary Beta