> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Noah virtual accounts and payouts

> Use portal.ramps.noah in the Flutter SDK for Noah KYC, payins, payouts, and quotes through the Portal Client API.

The Flutter SDK exposes [Noah](/integrations/On-Off-Ramp/noah) virtual accounts and global payouts through `portal.ramps.noah`. Each method forwards to the native iOS or Android SDK, which calls Portal's Noah integration on the [Client API](/apis/quickstart) using your client API key. You do not call Noah's servers directly from the app.

<Note>
  For dashboard setup, signing keys, and supported CAIP-2 networks, see [Noah integration overview](/integrations/On-Off-Ramp/noah). For HTTP shapes and webhooks, see the [Noah workflow guides](/integrations/On-Off-Ramp/noah#workflow-guides) and [Noah Business API / EMM documentation](https://docs.noah.com/).
</Note>

## Prerequisites

* An [initialized `Portal` client](./getting-started) with a wallet and API access.
* Noah enabled for your Portal environment and [configured in the dashboard](/integrations/On-Off-Ramp/noah).
* For payins and payouts, the end user must complete [Noah KYC](/integrations/On-Off-Ramp/noah-kyc) with approved status before those flows succeed.

## Architecture

| Layer | Role |
| - | - |
| Your app | Calls `portal.ramps.noah.*` from Dart |
| Flutter SDK | `PortalNoah` at `portal.ramps.noah` forwards each call over the platform channel |
| Native SDK | `portal.ramps.noah` in PortalSwift (iOS) or portal-android (Android) issues the authenticated HTTP request |
| Portal API | `POST/GET …/api/v3/clients/me/integrations/noah/...` with the Portal client API key |
| Noah (via Portal) | Hosted KYC, banking rails, settlement |

All request and response types are exported from `package:portal_flutter/portal_flutter.dart`.

## Supported networks

Every `network` parameter takes a CAIP-2 chain identifier. Use the constants on `NoahNetwork` rather than string literals to avoid typos.

| Constant | CAIP-2 value | Noah network |
| - | - | - |
| `NoahNetwork.ethereum` | `eip155:1` | `Ethereum` |
| `NoahNetwork.ethereumSepolia` | `eip155:11155111` | `EthereumTestSepolia` |
| `NoahNetwork.base` | `eip155:8453` | `Base` |
| `NoahNetwork.baseSepolia` | `eip155:84532` | `BaseTestSepolia` |
| `NoahNetwork.polygon` | `eip155:137` | `PolygonPos` |
| `NoahNetwork.polygonAmoy` | `eip155:80002` | `PolygonTestAmoy` |
| `NoahNetwork.gnosis` | `eip155:100` | `Gnosis` |
| `NoahNetwork.gnosisChiado` | `eip155:10200` | `GnosisTestChiado` |
| `NoahNetwork.solana` | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `Solana` |
| `NoahNetwork.solanaDevnet` | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | `SolanaDevnet` |

The backend also accepts `solana:mainnet` and `solana:devnet` as aliases for the two Solana networks. It rejects any other value, including non-CAIP-2 strings such as `"ethereum"`, with a `400`.

<Tip>
  Testnet networks pair with sandbox-only test assets such as `USDC_TEST`.
</Tip>

## Types and responses

Successful responses use an envelope `{ data, metadataJson }`. For example, `NoahInitiateKycResponse` is `{ data: NoahInitiateKycData(hostedUrl), metadataJson }`. Every response also has a `metadata` getter that decodes `metadataJson` into a `Map<String, dynamic>?`.

Types that cross the platform channel follow a few Flutter-specific conventions:

* **Free-form JSON is passed as a string.** Fields with no fixed shape end in `Json`. On requests, encode the value with `jsonEncode` (`formJson`, `metadataJson`). The value must be a JSON object: invalid JSON, or an array or other non-object value, is rejected before any network call with the method's `NOAH_*_ERROR` code. An empty or whitespace-only string is treated as absent. On responses, read the decoded getter instead of the raw string:

  | Raw field | Decoded getter | Type |
  | - | - | - |
  | `NoahChannel.formSchemaJson` | `NoahChannel.formSchema` | `Map<String, dynamic>?` |
  | `NoahGetPayoutChannelFormData.formSchemaJson` | `NoahGetPayoutChannelFormData.formSchema` | `Map<String, dynamic>?` |
  | `NoahFormNextStep.schemaJson` | `NoahFormNextStep.schema` | `Map<String, dynamic>?` |
  | `NoahGetPayoutCountriesData.countriesJson` | `NoahGetPayoutCountriesData.countries` | `Map<String, List<String>>` |
  | `metadataJson` on every response | `metadata` | `Map<String, dynamic>?` |

  The getters return `null` (or an empty map for `countries`) when the value is absent or malformed, rather than throwing.

* **Enum-like fields are strings.** `customerType`, `capability`, `stepType`, and `comparisonOperator` are `String` fields. Use the Dart enums to produce and parse them: pass `.value` when building a request, and use `fromValue` to read a response. An unknown `customerType` or `capability` string is rejected before any network call with the method's `NOAH_*_ERROR` code. `comparisonOperator` is not checked by the SDK and is validated by the backend.

  | Enum | Values |
  | - | - |
  | `NoahCustomerType` | `individual` (`Individual`), `business` (`Business`) |
  | `NoahPaymentMethodCapability` | `payoutFrom` (`PayoutFrom`), `payinTo` (`PayinTo`), `payoutTo` (`PayoutTo`) |
  | `NoahFormNextStepType` | `ack` (`Ack`), `dataEntry` (`DataEntry`) |
  | `NoahComparisonOperator` | `eq` (`EQ`), `lteq` (`LTEQ`), `gteq` (`GTEQ`) |

* **Lists have nullable elements.** List fields are typed `List<T?>`, so iterate with `whereType<T>()`.

All nine methods return a `Future` and throw `PortalException` on failure. See [Error handling](#error-handling).

## initiateKyc

Starts hosted Noah onboarding. The SDK returns the URL but does not open it, so open `data.hostedUrl` in the system browser yourself. The example below uses the [`url_launcher`](https://pub.dev/packages/url_launcher) package. Validate **HTTPS** and the **hostname** against the checkout domains Noah documents for your environment (extend the example allowlist accordingly).

```dart theme={null}
import 'package:portal_flutter/portal_flutter.dart';
import 'package:url_launcher/url_launcher.dart';

final portal = Portal();

const allowedHosts = {
  'checkout.noah.com',
  'checkout.sandbox.noah.com',
  'staging-checkout.noah.com',
};

try {
  final response = await portal.ramps.noah.initiateKyc(
    NoahInitiateKycRequest(
      returnUrl: 'https://yourapp.example/noah/return',
      fiatOptions: [NoahFiatOption(fiatCurrencyCode: 'USD')],
      customerType: NoahCustomerType.individual.value,
    ),
  );

  final uri = Uri.tryParse(response.data.hostedUrl);
  if (uri != null && uri.scheme == 'https' && allowedHosts.contains(uri.host)) {
    await launchUrl(uri, mode: LaunchMode.externalApplication);
  } else {
    // Handle this however your app prefers — surface it, log it, or fail the flow.
    print('Noah returned an unexpected KYC URL: ${response.data.hostedUrl}');
  }
} on PortalException catch (e) {
  print('initiateKyc failed: [${e.code}] ${e.message}');
}
```

**Signature**

```dart theme={null}
Future<NoahInitiateKycResponse> initiateKyc(NoahInitiateKycRequest request)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `request.returnUrl` | `String` | Yes | HTTPS URL where Noah returns the user after onboarding. |
| `request.fiatOptions` | `List<NoahFiatOption?>?` | No | Fiat currencies to present in onboarding. |
| `request.customerType` | `String?` | No | Onboarding flow variant — `NoahCustomerType.individual.value` or `NoahCustomerType.business.value`. |
| `request.metadataJson` | `String?` | No | JSON-encoded object of opaque metadata, forwarded per API rules. |
| `request.formJson` | `String?` | No | JSON-encoded prefill payload for hosted forms. |

<Note>
  The `returnUrl` must be an HTTPS URL. Custom app schemes (for example `myapp://callback`) are not supported. For mobile applications, the recommended pattern is to use an HTTPS bridge page that redirects to a deep link after KYC completion.
</Note>

**Returns** — `NoahInitiateKycResponse`: `{ data: { hostedUrl: String }, metadataJson: String? }`.

<Warning>
  While the customer's KYC is pending, each call starts a new onboarding session and returns a new `hostedUrl`. Once KYC is approved or declined, it returns the stored `hostedUrl` without starting a new session.
</Warning>

<Note>
  This call only starts onboarding; KYC outcome and status changes arrive asynchronously via Noah **`Customer`** webhooks. See [Noah webhooks](/integrations/On-Off-Ramp/noah-webhooks).
</Note>

See also: [Noah KYC guide](/integrations/On-Off-Ramp/noah-kyc), [Noah hosted flows](https://docs.noah.com/).

## initiatePayin

Creates a fiat-to-stablecoin payin and returns bank instructions and a `payinId`. Use a [supported network](#supported-networks) and the user's wallet address as `destinationAddress`.

```dart theme={null}
final address = (await portal.getAddress(NoahNetwork.ethereumSepolia))!; // Requires an existing wallet

final payin = await portal.ramps.noah.initiatePayin(
  NoahInitiatePayinRequest(
    fiatCurrency: 'USD',
    cryptoCurrency: 'USDC_TEST',
    network: NoahNetwork.ethereumSepolia,
    destinationAddress: address,
  ),
);

print(payin.data.payinId);
// Show payin.data.bankDetails to the user as deposit instructions — do not log it.
```

**Signature**

```dart theme={null}
Future<NoahInitiatePayinResponse> initiatePayin(NoahInitiatePayinRequest request)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `request.fiatCurrency` | `String` | Yes | Fiat currency code (for example `USD`). |
| `request.cryptoCurrency` | `String` | Yes | Noah crypto asset code (for example stablecoin test symbols in sandbox). |
| `request.network` | `String` | Yes | CAIP-2 chain identifier. Use a [`NoahNetwork` constant](#supported-networks). |
| `request.destinationAddress` | `String` | Yes | Address that receives crypto after settlement. |
| `request.businessFees` | `Map<String?, NoahBusinessFee?>?` | No | Accepted for parity with the native SDKs, but ignored by Portal. Business fees come from the [Noah configuration](/integrations/On-Off-Ramp/noah) in the Portal Dashboard. |

**Returns** — `NoahInitiatePayinResponse`: `{ data: { payinId: String, bankDetails: NoahBankDetails }, metadataJson: String? }`.

`NoahBankDetails` includes:

* `paymentMethodId` — payment method identifier
* `paymentMethodType` — payment rail type, for example `BankSepa` or `IdentifierPix`
* `accountNumber` — bank account number
* `cryptoCurrency` — crypto currency for this payin
* `network` — network identifier
* `fee` — fee breakdown (`NoahFeeDetails`: `fiatCurrencyCode`, `totalFeePct`, `totalFeeBase`, `totalFeeMin`)
* `accountHolderName` — optional account holder name
* `bankCode` — optional bank routing or sort code
* `bankName` — optional bank name
* `bankAddress` — optional `NoahBankAddress` with `street`, `street2`, `city`, `postCode`, `state`, `country`
* `reference` — optional payment reference
* `relatedPaymentMethods` — optional `List<NoahBankToAddressRelatedPaymentMethod?>`

<Warning>
  `NoahBankDetails` carries sensitive financial data — `accountNumber`, `accountHolderName`, `bankCode`, and `bankAddress`. Render it to the user as deposit instructions and keep it in memory; do not write it to logs, crash reports, or analytics.
</Warning>

<Note>
  Payin lifecycle updates are asynchronous; track them with Noah **`FiatDeposit`** and **`Transaction`** webhooks, not by polling this SDK response. See [Noah webhooks](/integrations/On-Off-Ramp/noah-webhooks).
</Note>

See also: [Payins](/integrations/On-Off-Ramp/noah-payins), [FiatDeposit webhooks](https://docs.noah.com/api-concepts/webhooks/fiat-deposits/).

## simulatePayin

Simulates a fiat deposit against a payment method so you can exercise the payin flow end to end without moving real money. Use the `bankDetails.paymentMethodId` returned by `initiatePayin`.

```dart theme={null}
final response = await portal.ramps.noah.simulatePayin(
  NoahSimulatePayinRequest(
    paymentMethodId: payin.data.bankDetails.paymentMethodId,
    fiatAmount: '10',
    fiatCurrency: 'USD',
  ),
);

print(response.data.fiatDepositId);
```

**Signature**

```dart theme={null}
Future<NoahSimulatePayinResponse> simulatePayin(NoahSimulatePayinRequest request)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `request.paymentMethodId` | `String` | Yes | Payment method identifier from Noah. |
| `request.fiatAmount` | `String` | Yes | Fiat amount as a decimal string. |
| `request.fiatCurrency` | `String` | Yes | Fiat currency code. |

**Returns** — `NoahSimulatePayinResponse`: `{ data: { fiatDepositId: String, reference: String? }, metadataJson: String? }`.

<Warning>
  This endpoint is **sandbox-only**. Calling it against a production environment fails with a `400` client error — `Simulate fiat deposit is only available in sandbox environments`. Guard the call behind your app's own sandbox/production configuration — for example, the setting that selects your sandbox API key — rather than shipping it in a production code path. Don't rely on `kDebugMode` alone: a debug build can point at production, and a release build at sandbox.
</Warning>

## getPayoutCountries

Lists countries available for fiat payouts, keyed by country code with the supported fiat currency codes for each.

```dart theme={null}
final response = await portal.ramps.noah.getPayoutCountries();

for (final entry in response.data.countries.entries) {
  print('${entry.key}: ${entry.value.join(', ')}');
}
```

**Signature**

```dart theme={null}
Future<NoahGetPayoutCountriesResponse> getPayoutCountries()
```

**Returns** — `NoahGetPayoutCountriesResponse`: `{ data: { countriesJson: String }, metadataJson: String? }`. Read the map through the `data.countries` getter, typed `Map<String, List<String>>`.

## getPayoutChannels

Returns payout rails for a given crypto asset. Only `cryptoCurrency` is required; `country` and `fiatCurrency` narrow the results, and `fiatAmount` refines channel availability.

```dart theme={null}
final response = await portal.ramps.noah.getPayoutChannels(
  NoahGetPayoutChannelsRequest(
    cryptoCurrency: 'USDC_TEST',
    country: 'US',
    fiatCurrency: 'USD',
    fiatAmount: '10',
    pageSize: 10,
  ),
);

final channels = response.data.items.whereType<NoahChannel>().toList();
for (final channel in channels) {
  print('${channel.id} ${channel.paymentMethodType} ${channel.rate}');
}

// Pick the channel to pay out through
final channel = channels.first;
```

**Signature**

```dart theme={null}
Future<NoahGetPayoutChannelsResponse> getPayoutChannels(NoahGetPayoutChannelsRequest request)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `request.cryptoCurrency` | `String` | Yes | Crypto asset code for the payout leg. |
| `request.country` | `String?` | No | ISO country code (for example `US`). |
| `request.fiatCurrency` | `String?` | No | Fiat currency for the payout. |
| `request.fiatAmount` | `String?` | No | Amount string used for filtering or quotes. |
| `request.paymentMethodId` | `String?` | No | Filter to channels compatible with a saved payment method. |
| `request.pageSize` | `int?` | No | Page size between 1 and 100, validated server-side. |
| `request.pageToken` | `String?` | No | Pagination token returned from a previous response. |

**Returns** — `NoahGetPayoutChannelsResponse`: `{ data: { items: List<NoahChannel?>, pageToken: String? }, metadataJson: String? }`.

Each `NoahChannel` includes:

* `id` — channel identifier
* `paymentMethodCategory` — broad grouping such as `Bank`, `Card`, or `Identifier`
* `paymentMethodType` — payment rail type, for example `BankSepa` or `IdentifierPix`
* `fiatCurrency` — fiat currency code
* `country` — ISO country code
* `limits` — `NoahChannelLimits` with `minLimit` and optional `maxLimit`
* `rate` — exchange rate as a string
* `processingSeconds` — estimated settlement time
* `calculated` — optional `NoahChannelCalculated` with `totalFee`
* `paymentMethods` — optional `List<NoahChannelPaymentMethodDisplay?>`
* `processingTier` — optional settlement speed tier such as `Standard` or `Priority`
* `formSchemaJson` — optional inline JSON Schema for the channel's payout form; read it decoded through the `formSchema` getter
* `formMetadata` — optional `NoahFormMetadata` with `contentHash`
* `issuer` — optional issuer identifier

<Tip>
  When a channel already carries a `formSchema`, use it directly instead of calling `getPayoutChannelForm` — it saves a round-trip.
</Tip>

See also: [Payouts](/integrations/On-Off-Ramp/noah-payouts).

## getPayoutChannelForm

Loads the dynamic form schema for a channel so you can collect recipient fields before requesting a quote.

```dart theme={null}
final response = await portal.ramps.noah.getPayoutChannelForm(channel.id);

// Render form fields from `response.data.formSchema` per Noah's schema
print(response.data.formMetadata?.contentHash ?? 'no schema metadata');
```

**Signature**

```dart theme={null}
Future<NoahGetPayoutChannelFormResponse> getPayoutChannelForm(String channelId)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `channelId` | `String` | Yes | Channel identifier from `getPayoutChannels`. |

**Returns** — `NoahGetPayoutChannelFormResponse`: `{ data: { formSchemaJson: String?, formMetadata: NoahFormMetadata? }, metadataJson: String? }`. Read the schema decoded through the `data.formSchema` getter.

<Note>
  The form schema depends on the region the customer completed KYC in. Build the `formJson` you send to `getPayoutQuote` from the schema returned for that customer, not from a fixed payload.
</Note>

## getPayoutQuote

Requests fees and crypto amount estimates for a payout. Include `formJson` when the channel requires recipient data.

```dart theme={null}
import 'dart:convert';

// Recipient fields matching the channel's `formSchema`. The schema depends on
// the region the customer completed KYC in.
final form = <String, Object>{
  'AccountHolderName': {
    'AccountHolderType': 'Individual',
    'Name': {'FirstName': 'John', 'LastName': 'Doe'},
  },
  // ... other fields required by the schema
};

final quote = await portal.ramps.noah.getPayoutQuote(
  NoahGetPayoutQuoteRequest(
    channelId: channel.id,
    cryptoCurrency: 'USDC_TEST',
    fiatAmount: '10',
    formJson: jsonEncode(form),
  ),
);

print(quote.data.payoutId);
print(quote.data.formSessionId);
print(quote.data.cryptoAmountEstimate);
print(quote.data.cryptoAuthorizedAmount);
print(quote.data.totalFee);
```

**Signature**

```dart theme={null}
Future<NoahGetPayoutQuoteResponse> getPayoutQuote(NoahGetPayoutQuoteRequest request)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `request.channelId` | `String` | Yes | Payout channel id. |
| `request.cryptoCurrency` | `String` | Yes | Crypto asset for the quote. |
| `request.fiatAmount` | `String?` | One of `fiatAmount` or `cryptoAmount` | Fiat amount to receive, as a string. |
| `request.cryptoAmount` | `String?` | One of `fiatAmount` or `cryptoAmount` | Crypto amount to sell, as a string. |
| `request.quoted` | `bool?` | No | Request a signed, rate-locked quote. When `true`, the response includes a `quote` with `signedQuote` and `expiry`. |
| `request.formJson` | `String?` | No | JSON-encoded recipient fields from the channel form. |
| `request.fiatCurrency` | `String?` | No | Fiat currency override when needed. |
| `request.paymentMethodId` | `String?` | No | Payment method hint when applicable. |
| `request.formSessionId` | `String?` | No | Existing form session to continue, for multi-step forms. |
| `request.businessFee` | `NoahBusinessFee?` | No | Accepted for parity with the native SDKs, but ignored by Portal. Business fees come from the [Noah configuration](/integrations/On-Off-Ramp/noah) in the Portal Dashboard. |

### Choosing the amount denomination

`fiatAmount` and `cryptoAmount` are mutually exclusive. Set exactly one: the SDK checks this in Dart before calling the native SDK, and throws a `PortalException` with code `INVALID_PAYOUT_QUOTE_AMOUNT` if you pass both or neither.

```dart theme={null}
// Quote a fiat amount to receive
NoahGetPayoutQuoteRequest(channelId: channel.id, cryptoCurrency: 'USDC_TEST', fiatAmount: '10');

// Quote a crypto amount to sell
NoahGetPayoutQuoteRequest(channelId: channel.id, cryptoCurrency: 'USDC_TEST', cryptoAmount: '10.5');
```

**Returns** — `NoahGetPayoutQuoteResponse`: `data` includes `payoutId`, `totalFee`, `cryptoAmountEstimate`, `cryptoAuthorizedAmount`, `formSessionId`, and optional `cryptoCurrency`, `fiatCurrency`, `fiatAmount`, `rate`, `breakdown`, `quote` (`{ signedQuote, expiry }`), and `nextStep`.

Each `breakdown` entry is a `NoahTransactionBreakdownItem` with a `type` of `ChannelFee`, `BusinessFee`, or `Remaining`, plus an `amount`.

When the form has more screens to collect, `nextStep` is a `NoahFormNextStep` with a `stepId`, a `stepType` string (parse it with `NoahFormNextStepType.fromValue`), and the next screen's schema through the `schema` getter. Send the next screen with the returned `formSessionId`.

<Note>
  Set `quoted: true` if you intend to submit the payout with a `quoted` trigger — that variant requires the `signedQuote` returned here.
</Note>

## initiatePayout

Executes a payout after quoting. Returns the destination address and the on-chain deposit conditions your transfer must satisfy.

```dart theme={null}
import 'dart:math';

final sourceAddress = (await portal.getAddress(NoahNetwork.ethereumSepolia))!; // Requires an existing wallet
final expiry = DateTime.now().toUtc().add(const Duration(days: 1)).toIso8601String();

// Use a stable nonce per payout attempt and reuse it on retries (max 36 characters)
final random = Random.secure();
final nonce = List.generate(16, (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0')).join();

final response = await portal.ramps.noah.initiatePayout(
  NoahInitiatePayoutRequest(
    payoutId: quote.data.payoutId,
    sourceAddress: sourceAddress,
    expiry: expiry,
    nonce: nonce,
    network: NoahNetwork.ethereumSepolia,
  ),
);

print(response.data.destinationAddress ?? 'no destination address');
print(response.data.conditions?.length ?? 0);
```

**Signature**

```dart theme={null}
Future<NoahInitiatePayoutResponse> initiatePayout(NoahInitiatePayoutRequest request)
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `request.payoutId` | `String` | Yes | Identifier from `getPayoutQuote`. |
| `request.sourceAddress` | `String` | Yes | Address funding the crypto leg. |
| `request.expiry` | `String` | Yes | ISO-8601 expiry for the deposit authorization. |
| `request.nonce` | `String` | Yes | Stable nonce for this payout attempt; **reuse on retry** so repeated calls stay idempotent. Must be 36 characters or fewer. |
| `request.network` | `String` | Yes | CAIP-2 network for the deposit leg. Use a [`NoahNetwork` constant](#supported-networks). |
| `request.trigger` | `NoahOnchainDepositSourceTrigger?` | No | Explicit on-chain deposit trigger. See [Deposit source triggers](#deposit-source-triggers). |
| `request.businessFee` | `NoahBusinessFee?` | No | Accepted for parity with the native SDKs, but ignored by Portal. Business fees come from the [Noah configuration](/integrations/On-Off-Ramp/noah) in the Portal Dashboard. |

**Returns** — `NoahInitiatePayoutResponse`: `{ data: { destinationAddress: String?, conditions: List<NoahDepositSourceTriggerCondition?>?, ruleId: String? }, metadataJson: String? }`.

`destinationAddress` is optional because it is derived from `conditions[0].destinationAddress` and is `null` when that shape is missing. `ruleId` is returned for permanent and quoted triggers.

### Deposit source triggers

The payout must be authorized by a trigger. If you omit `trigger`, Portal builds a `single` trigger from the quote (an `EQ` amount condition on its `cryptoAmountEstimate`) using the `network`, `sourceAddress`, `expiry`, and `nonce` you passed. Pass `trigger` to use a `permanent` or `quoted` trigger, or your own amount conditions.

`NoahOnchainDepositSourceTrigger` has one nullable field per variant. Set **exactly one** of them — the SDK throws a `PortalException` with code `INVALID_PAYOUT_TRIGGER` before calling the native SDK if you set none or more than one.

| Field | Type | Use it for |
| - | - | - |
| `single` | `NoahSingleOnchainDepositSourceTriggerInput?` | One payout matching specific amount conditions on a network. |
| `permanent` | `NoahPermanentOnchainDepositSourceTriggerInput?` | A source that stays active across multiple deposits until expiry. Set `networkAgnostic` to match any supported network. |
| `quoted` | `NoahQuotedOnchainDepositSourceTriggerInput?` | A rate-locked payout. Requires the `signedQuote` from a `getPayoutQuote` call made with `quoted: true`. |

Use the same `sourceAddress`, `expiry`, and `nonce` on the trigger as on the request:

```dart theme={null}
final trigger = NoahOnchainDepositSourceTrigger(
  single: NoahSingleOnchainDepositSourceTriggerInput(
    conditions: [
      NoahSingleOnchainDepositSourceTriggerCondition(
        amountConditions: [
          NoahSingleOnchainDepositSourceTriggerAmountCondition(
            comparisonOperator: NoahComparisonOperator.eq.value,
            value: quote.data.cryptoAuthorizedAmount,
          ),
        ],
        network: NoahNetwork.ethereumSepolia,
      ),
    ],
    sourceAddress: sourceAddress,
    expiry: expiry,
    nonce: nonce,
  ),
);

final response = await portal.ramps.noah.initiatePayout(
  NoahInitiatePayoutRequest(
    payoutId: quote.data.payoutId,
    sourceAddress: sourceAddress,
    expiry: expiry,
    nonce: nonce,
    network: NoahNetwork.ethereumSepolia,
    trigger: trigger,
  ),
);
```

<Note>
  The SDK does not send the funding transaction. After this call returns `destinationAddress` and `conditions`, submit the on-chain transfer to satisfy them. You can do this with any wallet — including Portal's own [send method](./send-tokens) (`portal.sendAsset(...)`) on the same `Portal` instance, which builds, signs, and broadcasts in one call.
</Note>

<Note>
  This call initiates the payout flow; completion and failures are reported asynchronously via Noah **`Transaction`** webhooks. See [Noah webhooks](/integrations/On-Off-Ramp/noah-webhooks).
</Note>

See also: [Transaction events](https://docs.noah.com/api-concepts/webhooks/transactions/), [automated payout recipes](https://docs.noah.com/recipes/payout/automated-payouts).

## getPaymentMethods

Returns payment methods available to the customer, including a pagination token when more results are available.

```dart theme={null}
final response = await portal.ramps.noah.getPaymentMethods(
  request: NoahGetPaymentMethodsRequest(
    pageSize: 10,
    capability: NoahPaymentMethodCapability.payoutTo.value,
  ),
);

print(response.data.paymentMethods.length);
print(response.data.pageToken ?? 'no more pages');
```

Omit `request` to use server-side defaults:

```dart theme={null}
final response = await portal.ramps.noah.getPaymentMethods();
```

**Signature**

```dart theme={null}
Future<NoahGetPaymentMethodsResponse> getPaymentMethods({NoahGetPaymentMethodsRequest? request})
```

| Parameter | Type | Required | Description |
| - | - | - | - |
| `request.pageSize` | `int?` | No | Maximum number of payment methods to return. Must be an integer between 1 and 100 — validated server-side, and out-of-range values are rejected with a `400` rather than clamped. |
| `request.pageToken` | `String?` | No | Cursor from a previous response's `data.pageToken`. |
| `request.capability` | `String?` | No | Filter by capability. Use `NoahPaymentMethodCapability.<case>.value` — the wire values are `PayoutFrom`, `PayinTo`, and `PayoutTo`. Any other string is rejected with `NOAH_GET_PAYMENT_METHODS_ERROR`. |

**Returns** — `NoahGetPaymentMethodsResponse`: `{ data: { paymentMethods: List<NoahPaymentMethod?>, pageToken: String? }, metadataJson: String? }`.

Each `NoahPaymentMethod` includes `id`, `paymentMethodCategory`, `country`, and `displayDetails`, plus optional `customerId`, `capabilities`, `accountHolderDetails`, and `issuerDetails`.

## Error handling

All Noah methods throw a `PortalException` on failure. Catch it with `on PortalException catch (e)` and branch on `e.code`. Log or surface errors without printing full API responses in production if they might contain sensitive identifiers. Retry only idempotent reads unless your product team confirms otherwise.

| Code | Description |
| - | - |
| `NOT_INITIALIZED` | Portal was not initialized. |
| `INVALID_PAYOUT_QUOTE_AMOUNT` | `getPayoutQuote` was called with both or neither of `fiatAmount` and `cryptoAmount`. Thrown in Dart before the native call. |
| `INVALID_PAYOUT_TRIGGER` | `initiatePayout` was called with a `trigger` that sets none, or more than one, of `single`, `permanent`, and `quoted`. Thrown in Dart before the native call. |
| `NOAH_INITIATE_KYC_ERROR` | `initiateKyc` failed. |
| `NOAH_INITIATE_PAYIN_ERROR` | `initiatePayin` failed — for example an unsupported `network`, or the customer has not completed KYC. |
| `NOAH_SIMULATE_PAYIN_ERROR` | `simulatePayin` failed — for example when called against a production environment. |
| `NOAH_GET_PAYMENT_METHODS_ERROR` | `getPaymentMethods` failed — for example an out-of-range `pageSize`. |
| `NOAH_GET_PAYOUT_COUNTRIES_ERROR` | `getPayoutCountries` failed. |
| `NOAH_GET_PAYOUT_CHANNELS_ERROR` | `getPayoutChannels` failed. |
| `NOAH_GET_PAYOUT_CHANNEL_FORM_ERROR` | `getPayoutChannelForm` failed — for example an unknown `channelId`. |
| `NOAH_GET_PAYOUT_QUOTE_ERROR` | `getPayoutQuote` failed — for example missing required form fields, or an amount outside the channel's limits. |
| `NOAH_INITIATE_PAYOUT_ERROR` | `initiatePayout` failed — for example a `payoutId` that was already initiated, or an expired quote. |

The `NOAH_*_ERROR` codes are the same on iOS and Android. They also cover server errors, responses missing a required field, and input rejected before the network call — an unknown `customerType` or `capability` string, or a `formJson` / `metadataJson` that is not a JSON object. If Portal rejects the API key or Client Session Token, the call throws `CREDENTIAL_REJECTED` instead.

```dart theme={null}
try {
  final response = await portal.ramps.noah.getPayoutCountries();
  print(response.data.countries);
} on PortalException catch (e) {
  if (e.code == 'NOT_INITIALIZED') {
    print('Initialize Portal before calling Noah');
  } else {
    print('getPayoutCountries failed: [${e.code}] ${e.message}');
  }
}
```

<Note>
  `e.message` includes the backend's error on both platforms, but its format differs between iOS and Android. Branch on `e.code`, not on `e.message`.
</Note>

<Note>
  Noah methods do not accept a trace ID, because the native Noah APIs do not support one yet.
</Note>

## Related documentation

* Flutter SDK reference — [`initiateKyc`](../reference/noah-initiatekyc), [`initiatePayin`](../reference/noah-initiatepayin), [`simulatePayin`](../reference/noah-simulatepayin), [`getPayoutCountries`](../reference/noah-getpayoutcountries), [`getPayoutChannels`](../reference/noah-getpayoutchannels), [`getPayoutChannelForm`](../reference/noah-getpayoutchannelform), [`getPayoutQuote`](../reference/noah-getpayoutquote), [`initiatePayout`](../reference/noah-initiatepayout), [`getPaymentMethods`](../reference/noah-getpaymentmethods)
* [Noah integration overview](/integrations/On-Off-Ramp/noah)
* [KYC](/integrations/On-Off-Ramp/noah-kyc), [Payins](/integrations/On-Off-Ramp/noah-payins), [Payouts](/integrations/On-Off-Ramp/noah-payouts), [Webhooks](/integrations/On-Off-Ramp/noah-webhooks)
* [Noah docs — API concepts](https://docs.noah.com/api-concepts/transactions/)
* [Noah docs — authentication & signing](https://docs.noah.com/api-concepts/authentication/signing/)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.