---
updatedAt: 2026-08-17T19:00:01.000Z
agentTools:
  projectIndex: https://docs.moderntreasury.com/payments/llms.txt
---

# Stablecoin On-ramp

Stablecoin payments use the same primitives as fiat:

* Legal Entities to onboard end users
* Internal Accounts to hold USD and stablecoin balances
* Counterparties to represent external wallets and bank accounts
* Payment Orders to move funds across rails.

USD-to-stablecoin conversions are triggered via Payment Order type: `book transfers` .  Use webhooks to track lifecycle events.

***

## Recommended implementation order

Build in this sequence:

1. Onboard end users by creating Legal Entities and waiting for KYC/KYB approval.
2. Open USD and stablecoin Internal Accounts for each approved entity.
3. Register counterparties with external bank accounts for fiat and wallet addresses for stablecoin.
4. Build the on-ramp: fund a USD internal account, convert to stablecoins, and send them to an external wallet.
5. Subscribe to webhooks for Payment Order and Legal Entity lifecycle events.

***

## 1. Onboard end users

Every entity that holds funds on your platform needs a verified Legal Entity. For required fields and documents, see:

* [Onboard a Business](https://docs.moderntreasury.com/payments/docs/onboard-a-business)
* [Onboard an Individual](https://docs.moderntreasury.com/payments/docs/onboard-an-individual)

### Create a business or individual Legal Entity

The example below creates a business Legal Entity with a control person. In production, also include beneficial owners, required documents (articles of incorporation and proof of address), and any additional identifications. See the onboarding guide above for the complete field list.

```bash Create a business legal entity
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/legal_entities \
  -H 'content-type: application/json' \
  -d '{
    "legal_entity_type": "business",
    "business_name": "Acme Payments Inc.",
    "legal_structure": "corporation",
    "business_description": "Online marketplace for handmade goods",
    "addresses": [
      {
        "address_types": ["business"]
        "primary": true,
        "line1": "100 Market Street",
        "locality": "San Francisco",
        "region": "CA",
        "postal_code": "94105",
        "country": "US"
      }
    ],
    "date_formed": "2020-06-15",
    "expected_activity_volume": 100000000,
    "intended_use": "Accepting payments from international customers",
    "identifications": [
      {
        "id_type": "us_ein",
        "issuing_country": "US",
        "id_number": "123456789"
      }
    ],
    "documents": [
      {
        "document_type": "proof_of_address",
        "file_data": "<base64 encoded bytes>",
        "filename": "proof_of_address.pdf"
      }
    ]
  }'
```

### Wait for approval

Wait for the `activated` webhook before you open accounts. In production, Modern Treasury may manually review legal entities, so ask your account manager for expected SLAs. In sandbox, legal entities activate automatically.

## 2. Open accounts

After the Legal Entity is approved, create Internal Accounts for USD and stablecoin balances. Each Internal Account is a programmable sub-account with its own account and routing number (for USD) or blockchain address (for stablecoins).

You will provision one USD Internal Account and one stablecoin Internal Account for the end user. The USD account will be created using your customer's `legal_entity_id` and the stablecoin account (USDC, USDG, etc.) will be created using your own organization's `legal_entity_id`:

### Create a USD Internal Account for your customer

```bash Create USD Internal Account
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/internal_accounts \
  -H 'content-type: application/json' \
  -d '{
    "name": "Acme Payments USD Account",
    "party_name": "Acme Payments Inc.",
    "currency": "USD",
    "legal_entity_id": "<CUSTOMER_LEGAL_ENTITY_ID>"
  }'
```

### Retrieved Organization (C1) Legal Entity ID

Use your Organization Legal Entity ID to provision the stablecoin Internal Account:

```bash Retrieve Org Legal Entity ID
curl --request GET \
  -u ORGANIZATION_ID:API_KEY \
  --url 'https://app.moderntreasury.com/api/legal_entities?business_name=<ORGANIZATION_NAME>' \
  -H 'content-type: application/json'
```

These details are also available within the legal entities UI in the Modern Treasury app.

### Create a stablecoin Internal Account

```bash Create stablecoin Internal Account
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/internal_accounts \
  -H 'content-type: application/json' \
  -d '{
    "name": "Acme Payments USDC Account",
    "party_name": "Acme Payments Inc.",
    "currency": "USDC",
    "legal_entity_id": "<ORGANIZATION_ENTITY_ID>",
    "requested_account_number_types": ["ethereum_address"]
  }'
```

The stablecoin Internal Account is created in `pending_activation` while the requested address is provisioned.

## Blockchain Network Selection

The `requested_account_number_types` value determines the blockchain network for the provisioned address. For example, `ethereum_address` provisions an EVM-compatible address. To use another network, request the matching `account_number_type`.

See [Supported stablecoins & blockchain networks](https://docs.moderntreasury.com/payments/docs/stablecoins#supported-stablecoins--blockchain-networks) before you create the counterparty.

<br />

```json Stablecoin IA Response (Pending Activation)
{
    "id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>",
    "object": "internal_account",
    "party_name": "Acme Payments Inc.",
    "name": "Acme Payments USDC Account",
    "currency": "USDC",
    "legal_entity_id": "5d95643d-1127-4a7c-9ef5-ad21a1d007c6",
    "account_details": [],
    "status": "pending_activation"
}
```

After the `internal_account.activated` webhook, the requested Ethereum address appears in `account_details`:

```json Stablecoin IA Response (After Activation)
{
    "id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>",
    "object": "internal_account",
    "party_name": "Acme Payments Inc.",
    "name": "Acme Payments USDC Account",
    "currency": "USDC",
    "legal_entity_id": "5d95643d-1127-4a7c-9ef5-ad21a1d007c6",
    "account_details": [
        {
            "account_number": "<ETHEREUM_ADDRESS>",
            "account_number_type": "ethereum_address"
        }
    ],
    "status": "active"
}
```

For the full list of supported stablecoins and blockchain networks, see [Stablecoins](https://docs.moderntreasury.com/payments/docs/stablecoins).

***

## 3. Set up counterparties

For the on-ramp flow, create a wallet counterparty for the external stablecoin wallet you will send to:

### Create a wallet counterparty

```bash Create wallet counterparty
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/counterparties \
  -H 'content-type: application/json' \
  -d '{
    "name": "Vendor Wallet",
    "accounts": [
      {
        "account_details": [
          {
            "account_number": "<COUNTERPARTY_WALLET_ADDRESS>",
            "account_number_type": "ethereum_address"
          }
        ]
      }
    ]
  }'
```

For the full list of supported `account_number_type` values, see [Account Detail](https://docs.moderntreasury.com/platform/reference/account-detail-object).

If you also need a bank counterparty for fiat funding, see [Create a Counterparty](https://docs.moderntreasury.com/platform/reference/create-a-counterparty).

***

## 4. On-ramp: fund USD internal account, convert to stablecoins, and send

![](https://files.readme.io/ce6b52f2989cf368451136003f74cc668eb466d1ae150bb666267d8e3d7eb35a-image.png)

Complete the on-ramp in three steps: fund a USD Internal Account, convert USD to stablecoins, and send the stablecoins to an external wallet.

### Step A: Fund the USD Internal Account

There are two funding patterns depending on who initiates the transfer.

**Pull (you initiate the debit).** Create a Payment Order that pulls funds from the external party's bank account into your USD Internal Account:

```bash
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/payment_orders \
  -H 'content-type: application/json' \
  -d '{
    "type": "ach",
    "amount": 100000,
    "direction": "debit",
    "currency": "USD",
    "originating_account_id": "<CUST_USD_INTERNAL_ACCOUNT_ID>",
    "receiving_account_id": "<BANK_EXTERNAL_ACCOUNT_ID>"
  }'
```

**Push (sender credits your account).** The sender initiates the transfer to your USD Internal Account. No Payment Order is created on your side — the funds arrive as an inbound payment. This pattern applies to ACH credit, wire, and RTP / FedNow.

Share your USD Internal Account's routing and account numbers with the sender. Retrieve them from the Internal Account object:

```bash
curl --request GET \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/internal_accounts/<USD_INTERNAL_ACCOUNT_ID>
```

Wait for the completed webhook before proceeding to the conversion step. The webhook you receive depends on the funding pattern:

```json Pull (payment_order.completed)
{
  "event": "completed",
  "data": {
    "id": "<PAYMENT_ORDER_ID>",
    "object": "payment_order",
    "type": "ach",
    "status": "completed"
  }
}
```
```Text Push (incoming_payment_detail.completed)
{
  "event": "completed",
  "data": {
    "id": "<INCOMING_PAYMENT_DETAIL_ID>",
    "object": "incoming_payment_detail",
    "type": "ach",
    "status": "completed",
    "amount": 100000,
    "currency": "USD",
    "direction": "credit",
    "internal_account_id": "<CUST_USD_INTERNAL_ACCOUNT_ID>"
  }
}
```

Settlement timing varies by rail. See [Originating Payments](https://docs.moderntreasury.com/payments/docs/making-payments) for details on each payment type.

<Callout icon="💡" theme="default">
  **Simulation:** To test inbound fiat funding events in Sandbox, simulate an Incoming Payment Detail at [Simulate Incoming Payment Detail](https://docs.moderntreasury.com/platform/reference/simulate-incoming-payment-detail-1).
</Callout>

### Step B: Convert USD to stablecoins (book transfer)

Once the USD has settled, convert it to stablecoins with a book transfer between Internal Accounts.

```bash
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/payment_orders \
  -H 'content-type: application/json' \
  -d '{
    "type": "book",
    "amount": 100000,
    "direction": "credit",
    "currency": "USDC",
    "originating_account_id": "<ORG_USD_INTERNAL_ACCOUNT_ID>",
    "receiving_account_id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>"
  }'
```

Wait for the `payment_order.completed` webhook before you send the stablecoins:

```json
{
  "event": "completed",
  "data": {
    "id": "<PAYMENT_ORDER_ID>",
    "object": "payment_order",
    "type": "book",
    "status": "completed"
  }
}
```

### Step C: Send stablecoins to an external wallet

After the conversion settles, send stablecoins to the counterparty's external wallet:

```bash
curl --request POST \
  -u ORGANIZATION_ID:API_KEY \
  --url https://app.moderntreasury.com/api/payment_orders \
  -H 'content-type: application/json' \
  -d '{
    "type": "stablecoin",
    "amount": 100000,
    "direction": "credit",
    "currency": "USDC",
    "originating_account_id": "<ORG_USDC_INTERNAL_ACCOUNT_ID>",
    "receiving_account_id": "<WALLET_EXTERNAL_ACCOUNT_ID>"
  }'
```

<Callout icon="💡" theme="default">
  `amount` uses the coin's smallest unit as an integer. For example, USDC uses 6 decimals, so `10000000` = 10.00 USDC. Stablecoin wallet addresses are validated and screened before payment execution.
</Callout>

<br />

***

If you use ACH debits for funding, Modern Treasury offers [Accelerated ACH Debit Settlement](https://docs.moderntreasury.com/payments/docs/ach#accelerated-ach-debit-settlement) to settle faster than the standard two days.