Onboard a merchant and accept a card payment
In this recipe, a merchant (a business account) accepts a card payment from a customer (an individual account). The customer's card is the source, the merchant's wallet is the destination, and your partner account creates the transfer. When you're done, you'll have a working card checkout in test mode that you can extend with refunds, disputes, and payouts.
Prerequisites
- Everything in Before you start: test mode API keys, authentication headers, your partner account ID, and a webhook endpoint
- Card numbers from the test mode guide
- To link cards with the API instead of the card link Drop, an attestation of PCI compliance on file with Moov
Steps
Create the merchant's business account
Create a business account with the create account endpoint. Include the business details Moov needs for verification and request collect-funds.card-payments and wallet.balance up front. Card payments settle into the merchant's Moov wallet, so the merchant needs both.
curl -X POST "https://api.moov.io/accounts" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00" \
-d '{
"accountType": "business",
"profile": {
"business": {
"legalBusinessName": "Whole Body Fitness LLC",
"businessType": "llc",
"address": {
"addressLine1": "123 Main Street",
"city": "Boulder",
"stateOrProvince": "CO",
"postalCode": "80301",
"country": "US"
},
"phone": { "number": "8185551212", "countryCode": "1" },
"email": "amanda@classbooker.dev",
"website": "www.wholebodyfitnessgym.com",
"taxID": { "ein": { "number": "123-45-6789" } },
"industryCodes": { "naics": "713940", "sic": "7991", "mcc": "7997" }
}
},
"capabilities": ["collect-funds.card-payments", "wallet.balance"],
"termsOfService": { "token": "{tos-token}" },
"settings": {
"cardPayment": { "statementDescriptor": "Whole Body Fitness" }
}
}'
Save the accountID from the response. The termsOfService token records the merchant's acceptance of Moov's platform agreement. Generate it in the merchant's browser with the terms of service Drop, or leave it out here and add it later with the update account endpoint.
If you'd rather not collect business details yourself, send the merchant a hosted onboarding link instead and skip to step 4 once its capabilities are enabled. Deeper guide: create a business account.
Add representatives
Before a business can collect funds, Moov verifies its representatives. Add at least one controller with the create representative endpoint, plus every owner with 25% or more ownership.
curl -X POST "https://api.moov.io/accounts/{merchantAccountID}/representatives" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00" \
-d '{
"name": { "firstName": "Amanda", "lastName": "Yang" },
"email": "amanda@classbooker.dev",
"phone": { "number": "8185551212", "countryCode": "1" },
"address": {
"addressLine1": "12 Main Street",
"city": "Boulder",
"stateOrProvince": "CO",
"postalCode": "80301",
"country": "US"
},
"birthDate": { "day": 10, "month": 11, "year": 1985 },
"governmentID": { "ssn": { "full": "111111111", "lastFour": "1111" } },
"responsibilities": {
"isController": true,
"isOwner": true,
"ownershipPercentage": 100,
"jobTitle": "CEO"
}
}'
When you've added everyone, tell Moov the list is complete by setting profile.business.ownersProvided to true with the update account endpoint. Deeper guide: business representatives.
Submit underwriting and confirm capabilities
collect-funds.card-payments stays pending until Moov has the underwriting it needs. Submit the merchant's expected activity with the underwriting POST endpoint. This example is a low-volume, US-only, mostly online merchant.
curl -X POST "https://api.moov.io/accounts/{merchantAccountID}/underwriting" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00" \
-d '{
"geographicReach": "us-only",
"collectFunds": {
"cardPayments": {
"estimatedActivity": { "monthlyVolumeRange": "under-10k" },
"currentlyAcceptsCards": false,
"cardAcceptanceMethods": {
"inPersonPercentage": 0,
"onlinePercentage": 100,
"mailOrPhonePercentage": 0
},
"fulfillment": {
"method": "subscription-or-membership",
"timeframe": "recurring-schedule"
},
"refundPolicy": "prorated-refund"
}
}
}'
Then check the merchant's capabilities with the list capabilities endpoint (GET /accounts/{merchantAccountID}/capabilities). Anything still required appears in requirements.currentlyDue. Keep submitting data until collect-funds.card-payments has status: enabled. Deeper guides: underwriting and capabilities.
Create the customer's account
The cardholder needs a Moov account to own the card. An individual account with only the transfers capability, which Moov enables automatically, is enough to pay a merchant.
curl -X POST "https://api.moov.io/accounts" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00" \
-d '{
"accountType": "individual",
"profile": {
"individual": {
"name": { "firstName": "Jules", "lastName": "Jackson" },
"email": "julesjacksonyoga@moov.io"
}
},
"foreignID": "customer-8675309"
}'
Save the customer's accountID. Use foreignID to map the Moov account to the customer record in your system. Deeper guide: create an individual account.
Link the customer's card
Most platforms collect card details with the card link Drop, which sends card data straight from the customer's browser to Moov so it never touches your servers. If you've attested to PCI compliance, you can link the card with the link card endpoint instead:
curl -X POST "https://api.moov.io/accounts/{customerAccountID}/cards" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00" \
-H "X-Wait-For: payment-method" \
-d '{
"cardNumber": "4111111111111111",
"cardCvv": "111",
"expiration": { "month": "12", "year": "30" },
"holderName": "Jules Jackson",
"billingAddress": { "postalCode": "11111" },
"merchantAccountID": "{merchantAccountID}"
}'
Moov verifies the card with the card network when you link it. With X-Wait-For: payment-method, the response includes the card's new payment methods. A 422 response means the card failed validation or the issuer declined verification. In test mode, CVV 111 and postal code 11111 return match results. Deeper guide: accept card payments.
Find the payment methods
A transfer moves money between payment methods, not accounts. You need the customer's card-payment payment method (the source) and the merchant's moov-wallet payment method (the destination). Use the list payment methods endpoint on each account, filtering by type:
curl -X GET "https://api.moov.io/accounts/{customerAccountID}/payment-methods?paymentMethodType=card-payment" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00"
Repeat for the merchant with paymentMethodType=moov-wallet. Save each paymentMethodID. If the merchant has no moov-wallet payment method, check that wallet.balance is enabled. Deeper guide: payment methods.
Create the card transfer
Create the payment with the create transfer endpoint, using your partner account ID in the path. Every transfer needs a unique X-Idempotency-Key; reuse the same key if you retry. X-Wait-For: rail-response returns the card network's answer synchronously, which is useful when a customer is waiting on a checkout screen.
curl -X POST "https://api.moov.io/accounts/{partnerAccountID}/transfers" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00" \
-H "X-Idempotency-Key: 5b7a3c1e-2f4d-4e8a-9c6b-1d2e3f4a5b6c" \
-H "X-Wait-For: rail-response" \
-d '{
"source": { "paymentMethodID": "{customerCardPaymentMethodID}" },
"destination": { "paymentMethodID": "{merchantWalletPaymentMethodID}" },
"amount": { "currency": "USD", "value": 4999 },
"description": "Monthly membership"
}'
Amounts are in cents, so 4999 is $49.99. Check status and source.cardDetails.status in the response: confirmed means the issuer approved the charge. A 202 response means the network didn't answer within the timeout. Don't treat it as a success or a failure; wait for the webhook. Deeper guides: idempotency and events and statuses.
Track the payment with webhooks
Subscribe to transfer.created and transfer.updated so you hear about every status change without polling. Register your endpoint in the Dashboard under Developers > Webhooks, choose those events (plus capability.updated and dispute.created for this flow), and copy the signing secret.
When transfer.updated arrives, verify the X-Signature header, then fetch the transfer with the get transfer endpoint to read its current status and rail details:
curl -X GET "https://api.moov.io/accounts/{partnerAccountID}/transfers/{transferID}" \
-H "Authorization: Bearer {token}" \
-H "X-Moov-Version: v2026.07.00"
Each event carries the transferID and status. Deeper guides: handle webhooks end to end, set up webhooks, and check webhook signatures.
What to watch for
| Signal | What it means | What to do |
|---|---|---|
capability.updated with collect-funds.card-payments enabled |
The merchant can accept cards | Turn on checkout for the merchant |
requirements.currentlyDue not empty |
Verification or underwriting needs more data | Collect it, or send a resolution link |
transfer.updated with source.cardDetails.status confirmed |
The issuer approved the charge | Show a receipt; fulfill the order |
transfer.updated with status completed |
Funds settled to the merchant's wallet, usually by the next business day | Funds can be paid out or swept |
transfer.updated with status failed |
The card was declined or a network error occurred | Read failureReason and the decline code; see declines |
dispute.created |
The cardholder disputed the charge | Respond before the deadline; see disputes |
To test failures, link one of the declined test cards and repeat step 7.
Next steps
Cancel, refund, or reverse a card payment and track the outcome.
Move the merchant's wallet balance to their bank account.
Settle the merchant's wallet to their bank account automatically every day.
Add digital wallets to the same checkout.