# Onboard a merchant and accept a card payment

> Create a business account, enable collect-funds.card-payments, link a customer's card, create a card transfer, and track it with webhooks.

> AI agents: search and read all Moov docs with the docs MCP server at https://docs.moov.io/mcp (no API key). Setup: https://docs.moov.io/guides/developer-tools/mcp-ai/docs-mcp/index.md

Onboard a merchant, link a customer's card, charge it, and follow the payment until the funds land in the merchant's Moov wallet.

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](/guides/get-started/recipes/index.md#before-you-start): test mode API keys, authentication headers, your partner account ID, and a webhook endpoint
- Card numbers from the [test mode](/guides/get-started/api-authentication/test-mode/index.md#link-cards--card-acceptance) 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](/api/moov-accounts/accounts/create/index.md). 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.

```zsh
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](/guides/accounts/requirements/platform-agreement/index.md). Generate it in the merchant's browser with the [terms of service Drop](/moovjs/drops/terms-of-service/index.md), 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](/guides/accounts/hosted-onboarding/index.md) link instead and skip to step 4 once its capabilities are enabled. Deeper guide: [create a business account](/guides/accounts/create-accounts/business/index.md).

### Add representatives

Before a business can collect funds, Moov verifies its representatives. Add at least one controller with the create representative [endpoint](/api/moov-accounts/representatives/create/index.md), plus every owner with 25% or more ownership.

```zsh
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](/api/moov-accounts/accounts/patch/index.md). Deeper guide: [business representatives](/guides/accounts/requirements/business-representatives/index.md).

### 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](/api/moov-accounts/underwriting/post/index.md). This example is a low-volume, US-only, mostly online merchant.

```zsh
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](/api/moov-accounts/capabilities/list/index.md) (`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](/guides/accounts/requirements/underwriting/index.md) and [capabilities](/guides/accounts/capabilities/enablement/index.md).

### 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.

```zsh
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](/guides/accounts/create-accounts/individual/index.md).

### Link the customer's card

Most platforms collect card details with the [card link Drop](/moovjs/drops/card-link/index.md), 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](/api/sources/cards/create/index.md) instead:

```zsh
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](/guides/accept-payments/cards/online/index.md).

### 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](/api/sources/payment-methods/list/index.md) on each account, filtering by type:

```zsh
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](/guides/money-movement/payment-methods/index.md).

### Create the card transfer

Create the payment with the create transfer [endpoint](/api/money-movement/transfers/create/index.md), 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.

```zsh
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](/guides/money-movement/idempotency/index.md) and [events and statuses](/guides/money-movement/events-and-statuses/index.md#create-transfer-responses).

### 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 &gt; 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](/api/money-movement/transfers/get/index.md) to read its current status and rail details:

```zsh
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](/guides/get-started/recipes/webhooks/index.md), [set up webhooks](/guides/webhooks/set-up-webhooks/index.md), and [check webhook signatures](/guides/webhooks/check-webhook-signatures/index.md).

## 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](/guides/accounts/resolution-links/index.md)                        |
| `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](/guides/accept-payments/cards/declines/index.md) |
| `dispute.created`                                                 | The cardholder disputed the charge                                       | Respond before the deadline; see [disputes](/guides/accept-payments/cards/disputes/index.md)               |

To test failures, link one of the [declined test cards](/guides/get-started/api-authentication/test-mode/index.md#declined-card-transfers) and repeat step 7.

## Next steps

- [Refunds and cancellations](/guides/get-started/recipes/refunds-cancellations/index.md): Cancel, refund, or reverse a card payment and track the outcome.
- [Pay out the merchant](/guides/get-started/recipes/bank-payout/index.md): Move the merchant's wallet balance to their bank account.
- [Sweeps](/guides/wallets/sweeps/index.md): Settle the merchant's wallet to their bank account automatically every day.
- [Apple Pay and Google Pay](/guides/accept-payments/cards/apple-pay/index.md): Add digital wallets to the same checkout.

