> ## 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 Android SDK for Noah KYC, payins, payouts, and quotes through the Portal Client API.

The Android SDK exposes [Noah](/integrations/On-Off-Ramp/noah) virtual accounts and global payouts through `portal.ramps.noah`. Each method issues HTTP requests through `portal.api` to 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.*`                                                                                       |
| Android SDK       | The `Noah` class at `portal.ramps.noah` issues authenticated HTTP requests through `portal.api` to the Portal API |
| Portal API        | `POST/GET …/api/v3/clients/me/integrations/noah/...` with the Portal client API key                               |
| Noah (via Portal) | Hosted KYC, banking rails, settlement                                                                             |

Prefer `portal.ramps.noah` over lower-level APIs. All request and response types live in `io.portalhq.android.api.data.noah`.

## Supported networks

Every `network` parameter takes a CAIP-2 chain identifier. Use the constants on `NoahNetwork` rather than string literals so a typo is a compile error instead of a runtime failure.

| Constant                       | CAIP-2 value                              | Noah network          |
| ------------------------------ | ----------------------------------------- | --------------------- |
| `NoahNetwork.ETHEREUM`         | `eip155:1`                                | `Ethereum`            |
| `NoahNetwork.ETHEREUM_SEPOLIA` | `eip155:11155111`                         | `EthereumTestSepolia` |
| `NoahNetwork.BASE`             | `eip155:8453`                             | `Base`                |
| `NoahNetwork.BASE_SEPOLIA`     | `eip155:84532`                            | `BaseTestSepolia`     |
| `NoahNetwork.POLYGON`          | `eip155:137`                              | `PolygonPos`          |
| `NoahNetwork.POLYGON_AMOY`     | `eip155:80002`                            | `PolygonTestAmoy`     |
| `NoahNetwork.GNOSIS`           | `eip155:100`                              | `Gnosis`              |
| `NoahNetwork.GNOSIS_CHIADO`    | `eip155:10200`                            | `GnosisTestChiado`    |
| `NoahNetwork.SOLANA`           | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `Solana`              |
| `NoahNetwork.SOLANA_DEVNET`    | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | `SolanaDevnet`        |

Any other value is rejected with a **400 Bad Request** (`PortalException.Api.HttpBadRequest`), because the chain is not mapped to a Noah network. A non-CAIP-2 string such as `"Ethereum"` is rejected earlier still, by the `[namespace]:[reference]` format check. Read the specific reason from the exception's `rawBody` rather than matching on message text — the wording is not a stable API.

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

## Types and responses

Every response is an envelope — `data class NoahXxxResponse(val data: T, val metadata: NoahResponseMetadata? = null)`, where `NoahResponseMetadata` is a type alias for `Map<String, Any>`.

All nine methods are `suspend` and return **`Result<T>`**. They do not throw, and they do not return a bare value. Handle both outcomes with `onSuccess` / `onFailure`:

```kotlin theme={null}
portal.ramps.noah.getPayoutCountries()
  .onSuccess { response -> Log.d("Noah", "${response.data.countries}") }
  .onFailure { error -> Log.e("Noah", "Failed", error) }
```

Every example below uses `onSuccess` / `onFailure`, `getOrNull()`, or `getOrThrow()`. Do not wrap these calls in `try/catch` — the failure is inside the `Result`, not on the stack. See [Error handling](#error-handling).

## initiateKyc

Starts hosted Noah onboarding. Open `data.hostedUrl` in the system browser. Validate **HTTPS** and the **hostname** against the checkout domains Noah documents for your environment (extend the example allowlist accordingly).

```kotlin theme={null}
import android.content.Intent
import android.net.Uri
import android.util.Log
import io.portalhq.android.api.data.noah.NoahCustomerType
import io.portalhq.android.api.data.noah.NoahFiatOption
import io.portalhq.android.api.data.noah.NoahInitiateKycRequest

val allowedHosts = setOf(
  "checkout.noah.com",
  "checkout.sandbox.noah.com",
  "staging-checkout.noah.com",
)

portal.ramps.noah.initiateKyc(
  NoahInitiateKycRequest(
    returnUrl = "https://yourapp.example/noah/return",
    fiatOptions = listOf(NoahFiatOption(fiatCurrencyCode = "USD")),
    customerType = NoahCustomerType.INDIVIDUAL,
  )
).onSuccess { response ->
  val uri = Uri.parse(response.data.hostedUrl)
  if (uri.scheme != "https" || uri.host !in allowedHosts) {
    Log.e("Noah", "Invalid KYC URL host or scheme")
    return@onSuccess
  }
  startActivity(Intent(Intent.ACTION_VIEW, uri))
}.onFailure { error ->
  Log.e("Noah", "initiateKyc failed", error)
}
```

**Signature**

```kotlin theme={null}
suspend fun initiateKyc(request: NoahInitiateKycRequest): Result<NoahInitiateKycResponse>
```

| 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 advertise during onboarding.           |
| `request.customerType` | `NoahCustomerType?`     | No       | Onboarding flow variant — `INDIVIDUAL` or `BUSINESS`.     |
| `request.metadata`     | `Map<String, Any>?`     | No       | Client-supplied metadata forwarded to Noah.               |
| `request.form`         | `Map<String, Any>?`     | No       | Pre-filled form payload mirrored back to the hosted flow. |

<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 } }`.

<Warning>
  This endpoint is **idempotent**. If a Noah customer record already exists for the client, the previously stored `hostedUrl` is returned regardless of KYC status (`Pending`, `Submitted`, `Approved`, `Declined`). Calling it a second time does not restart onboarding and does not mint a fresh URL.
</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`.

```kotlin theme={null}
import io.portalhq.android.api.data.noah.NoahInitiatePayinRequest
import io.portalhq.android.api.data.noah.NoahNetwork

portal.ramps.noah.initiatePayin(
  NoahInitiatePayinRequest(
    fiatCurrency = "USD",
    cryptoCurrency = "USDC_TEST",
    network = NoahNetwork.SOLANA_DEVNET,
    destinationAddress = "SoLAddr1111111111111111111111111111111111111",
  )
).onSuccess { response ->
  Log.d("Noah", "Payin: ${response.data.payinId}")
  // Show bankDetails to the user as deposit instructions — do not log it.
  showDepositInstructions(response.data.bankDetails)
}.onFailure { error ->
  Log.e("Noah", "initiatePayin failed", error)
}
```

**Signature**

```kotlin theme={null}
suspend fun initiatePayin(request: NoahInitiatePayinRequest): Result<NoahInitiatePayinResponse>
```

| Parameter                    | Type                            | Required | Description                                                                                          |
| ---------------------------- | ------------------------------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `request.fiatCurrency`       | `String`                        | Yes      | ISO-4217 fiat currency code the payin is denominated in.                                             |
| `request.cryptoCurrency`     | `String`                        | Yes      | Crypto asset symbol the payin resolves into.                                                         |
| `request.network`            | `String`                        | Yes      | CAIP-2 chain id for `destinationAddress`. Use a [`NoahNetwork` constant](#supported-networks).       |
| `request.destinationAddress` | `String`                        | Yes      | Address that receives the resulting crypto balance.                                                  |
| `request.businessFees`       | `Map<String, NoahBusinessFee>?` | No       | Per-payment-method business fees, keyed by payment method type. Forwarded to Noah as `BusinessFees`. |

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

`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 `Log`, 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 landing against a payment method so you can exercise the payin flow end to end without moving real money.

```kotlin theme={null}
import io.portalhq.android.api.data.noah.NoahSimulatePayinRequest

portal.ramps.noah.simulatePayin(
  NoahSimulatePayinRequest(
    paymentMethodId = "pm-1",
    fiatAmount = "10",
    fiatCurrency = "USD",
  )
).onSuccess { response ->
  Log.d("Noah", "Simulated deposit: ${response.data.fiatDepositId}")
}.onFailure { error ->
  Log.e("Noah", "simulatePayin failed", error)
}
```

**Signature**

```kotlin theme={null}
suspend fun simulatePayin(request: NoahSimulatePayinRequest): Result<NoahSimulatePayinResponse>
```

| Parameter                 | Type     | Required | Description                                              |
| ------------------------- | -------- | -------- | -------------------------------------------------------- |
| `request.paymentMethodId` | `String` | Yes      | Payment method id returned by `initiatePayin`.           |
| `request.fiatAmount`      | `String` | Yes      | Amount to simulate as deposited, as a string.            |
| `request.fiatCurrency`    | `String` | Yes      | ISO-4217 fiat currency code matching the payin currency. |

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

<Warning>
  This endpoint is **sandbox-only**. Calling it against a production environment fails with a **400 Bad Request**, surfacing as `PortalException.Api.HttpBadRequest` — not as a 403. Guard the call behind your own environment check rather than shipping it in a production code path.
</Warning>

## getPayoutCountries

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

```kotlin theme={null}
portal.ramps.noah.getPayoutCountries()
  .onSuccess { response ->
    response.data.countries.forEach { (country, currencies) ->
      Log.d("Noah", "$country: ${currencies.joinToString(", ")}")
    }
  }
  .onFailure { error -> Log.e("Noah", "getPayoutCountries failed", error) }
```

**Signature**

```kotlin theme={null}
suspend fun getPayoutCountries(): Result<NoahGetPayoutCountriesResponse>
```

**Returns** — `NoahGetPayoutCountriesResponse`: `{ data: { countries: 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` seeds channel-level fee calculations.

```kotlin theme={null}
import io.portalhq.android.api.data.noah.NoahGetPayoutChannelsRequest

portal.ramps.noah.getPayoutChannels(
  NoahGetPayoutChannelsRequest(
    cryptoCurrency = "USDC_TEST",
    country = "US",
    fiatCurrency = "USD",
    fiatAmount = "10",
    pageSize = 10,
  )
).onSuccess { response ->
  response.data.items.forEach { channel ->
    Log.d("Noah", "${channel.id} ${channel.paymentMethodType} ${channel.rate}")
  }
  response.data.pageToken?.let { Log.d("Noah", "Next page: $it") }
}.onFailure { error ->
  Log.e("Noah", "getPayoutChannels failed", error)
}
```

**Signature**

```kotlin theme={null}
suspend fun getPayoutChannels(request: NoahGetPayoutChannelsRequest): Result<NoahGetPayoutChannelsResponse>
```

| Parameter                 | Type      | Required | Description                                                    |
| ------------------------- | --------- | -------- | -------------------------------------------------------------- |
| `request.cryptoCurrency`  | `String`  | Yes      | Crypto asset symbol the payout originates from.                |
| `request.country`         | `String?` | No       | ISO-3166 country code, for example `US`.                       |
| `request.fiatCurrency`    | `String?` | No       | ISO-4217 fiat currency the payout is denominated in.           |
| `request.fiatAmount`      | `String?` | No       | Amount used to seed channel-level fee calculations.            |
| `request.paymentMethodId` | `String?` | No       | Saved payment method id to filter channels by.                 |
| `request.pageSize`        | `Int?`    | No       | Page size between 1 and 100, validated server-side.            |
| `request.pageToken`       | `String?` | No       | Pagination cursor from a previous response's `data.pageToken`. |

**Returns** — `NoahGetPayoutChannelsResponse`: `{ data: { items: List<NoahChannel>, pageToken: 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>`, only populated when a customer id was supplied
* `processingTier` — optional settlement speed tier such as `Standard` or `Priority`
* `formSchema` — optional inline JSON Schema for the channel's payout form
* `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. The `channelId` is URL-encoded as a single path segment, with spaces normalized to `%20`.

```kotlin theme={null}
portal.ramps.noah.getPayoutChannelForm("ch-1")
  .onSuccess { response ->
    // Render form fields from response.data.formSchema per Noah's schema
    Log.d("Noah", "Schema keys: ${response.data.formSchema?.keys}")
    Log.d("Noah", "Content hash: ${response.data.formMetadata?.contentHash}")
  }
  .onFailure { error -> Log.e("Noah", "getPayoutChannelForm failed", error) }
```

**Signature**

```kotlin theme={null}
suspend fun getPayoutChannelForm(channelId: String): Result<NoahGetPayoutChannelFormResponse>
```

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

**Returns** — `NoahGetPayoutChannelFormResponse`: `{ data: { formSchema: Map<String, Any>?, formMetadata: NoahFormMetadata? } }`.

## getPayoutQuote

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

```kotlin theme={null}
import io.portalhq.android.api.data.noah.NoahGetPayoutQuoteRequest

portal.ramps.noah.getPayoutQuote(
  NoahGetPayoutQuoteRequest.withFiatAmount(
    channelId = "ch-1",
    cryptoCurrency = "USDC_TEST",
    fiatAmount = "10",
  )
).onSuccess { response ->
  val quote = response.data
  Log.d("Noah", "Payout: ${quote.payoutId}")
  Log.d("Noah", "Send: ${quote.cryptoAuthorizedAmount}")
  Log.d("Noah", "Fee: ${quote.totalFee}")
  quote.breakdown?.forEach { Log.d("Noah", "  ${it.type}: ${it.amount}") }
}.onFailure { error ->
  Log.e("Noah", "getPayoutQuote failed", error)
}
```

**Signature**

```kotlin theme={null}
suspend fun getPayoutQuote(request: NoahGetPayoutQuoteRequest): Result<NoahGetPayoutQuoteResponse>
```

| Parameter                 | Type                | Required                              | Description                                                                                                          |
| ------------------------- | ------------------- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| `request.channelId`       | `String`            | Yes                                   | Channel id returned by `getPayoutChannels`.                                                                          |
| `request.cryptoCurrency`  | `String`            | Yes                                   | Crypto asset symbol funding the payout.                                                                              |
| `request.fiatAmount`      | `String?`           | One of `fiatAmount` or `cryptoAmount` | Fiat amount the user expects to receive.                                                                             |
| `request.cryptoAmount`    | `String?`           | One of `fiatAmount` or `cryptoAmount` | Crypto amount to sell.                                                                                               |
| `request.quoted`          | `Boolean?`          | No                                    | Request a signed, rate-locked quote. Required to later submit a `NoahQuotedOnchainDepositSourceTriggerInput` payout. |
| `request.form`            | `Map<String, Any>?` | No                                    | Form payload satisfying the schema from `getPayoutChannelForm`.                                                      |
| `request.fiatCurrency`    | `String?`           | No                                    | ISO-4217 fiat currency override.                                                                                     |
| `request.paymentMethodId` | `String?`           | No                                    | Saved payment method id to settle the payout to.                                                                     |
| `request.formSessionId`   | `String?`           | No                                    | Existing form session to continue, for multi-step forms.                                                             |
| `request.businessFee`     | `NoahBusinessFee?`  | No                                    | Business fee override. Forwarded to Noah as `BusinessFee`.                                                           |

### Choosing the amount denomination

`fiatAmount` and `cryptoAmount` are mutually exclusive. The primary constructor enforces this **at runtime** — passing both, or neither, throws `IllegalArgumentException` at construction, before any network call:

```kotlin theme={null}
init {
  require((fiatAmount == null) != (cryptoAmount == null)) {
    "Provide exactly one of fiatAmount or cryptoAmount"
  }
}
```

<Warning>
  Prefer the `withFiatAmount` and `withCryptoAmount` factory helpers over the primary constructor. They make the invalid combination unrepresentable at the call site rather than deferring it to a runtime `require` check. Both are annotated `@JvmStatic`, so they are also callable from Java.
</Warning>

```kotlin theme={null}
// Quote a fiat amount to receive
NoahGetPayoutQuoteRequest.withFiatAmount(
  channelId = "ch-1",
  cryptoCurrency = "USDC_TEST",
  fiatAmount = "10",
)

// Quote a crypto amount to sell
NoahGetPayoutQuoteRequest.withCryptoAmount(
  channelId = "ch-1",
  cryptoCurrency = "USDC_TEST",
  cryptoAmount = "10.5",
)
```

**Returns** — `NoahGetPayoutQuoteResponse`: 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`.

<Note>
  Set `quoted = true` if you intend to submit the payout with a `NoahQuotedOnchainDepositSourceTriggerInput` 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.

```kotlin theme={null}
import io.portalhq.android.api.data.noah.NoahInitiatePayoutRequest
import io.portalhq.android.api.data.noah.NoahNetwork
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.UUID

val expiry = Instant.now().plus(24, ChronoUnit.HOURS).truncatedTo(ChronoUnit.SECONDS).toString()

// Use a stable nonce per payout attempt and reuse it on retries (max 36 characters)
val nonce = UUID.randomUUID().toString().take(36)

portal.ramps.noah.initiatePayout(
  NoahInitiatePayoutRequest(
    payoutId = quote.payoutId,
    sourceAddress = "SoLAddr1111111111111111111111111111111111111",
    expiry = expiry,
    nonce = nonce,
    network = NoahNetwork.SOLANA_DEVNET,
  )
).onSuccess { response ->
  Log.d("Noah", "Destination: ${response.data.destinationAddress}")
  Log.d("Noah", "Conditions: ${response.data.conditions}")
}.onFailure { error ->
  Log.e("Noah", "initiatePayout failed", error)
}
```

<Note>
  `java.time` requires API 26 or [core library desugaring](https://developer.android.com/studio/write/java8-support-table). If your `minSdk` is lower and desugaring is not enabled, format the expiry with a UTC `SimpleDateFormat` instead.
</Note>

**Signature**

```kotlin theme={null}
suspend fun initiatePayout(request: NoahInitiatePayoutRequest): Result<NoahInitiatePayoutResponse>
```

| Parameter               | Type                               | Required | Description                                                                                                                         |
| ----------------------- | ---------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `request.payoutId`      | `String`                           | Yes      | Payout id returned by `getPayoutQuote`.                                                                                             |
| `request.sourceAddress` | `String`                           | Yes      | Crypto address that funds the payout.                                                                                               |
| `request.expiry`        | `String`                           | Yes      | ISO-8601 expiry timestamp for the trigger condition.                                                                                |
| `request.nonce`         | `String`                           | Yes      | Unique nonce that protects against replay. **Reuse it on retry** so repeated calls stay idempotent. Must be 36 characters or fewer. |
| `request.network`       | `String`                           | Yes      | CAIP-2 chain id for `sourceAddress`. Use a [`NoahNetwork` constant](#supported-networks).                                           |
| `request.trigger`       | `NoahOnchainDepositSourceTrigger?` | No       | Explicit deposit trigger override. See [Deposit source triggers](#deposit-source-triggers).                                         |
| `request.businessFee`   | `NoahBusinessFee?`                 | No       | Business fee override. Forwarded to Noah as `BusinessFee`.                                                                          |

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

`destinationAddress` is nullable because it is derived from `conditions[0].destinationAddress` and falls back to `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. Either supply one explicitly, or rely on a saved payment method attached to the quote via `paymentMethodId` — when `trigger` is omitted and the saved method is a single on-chain deposit source, a default trigger is synthesized from the `sourceAddress`, `expiry`, and `nonce` you passed.

`NoahOnchainDepositSourceTrigger` is a `sealed interface` with three implementations:

| Implementation                                  | Use it for                                                                                                                                                            |
| ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NoahSingleOnchainDepositSourceTriggerInput`    | One payout matching specific amount conditions on a network.                                                                                                          |
| `NoahPermanentOnchainDepositSourceTriggerInput` | A source that stays active across multiple deposits until expiry. Set `networkAgnostic` to match any supported network.                                               |
| `NoahQuotedOnchainDepositSourceTriggerInput`    | A rate-locked payout. Requires the `signedQuote` from a `getPayoutQuote` call made with `quoted = true`. Its `expiry` is nullable — the signed quote carries its own. |

Each implementation defaults its `type` property to the correct discriminator, so you do not need to set it:

```kotlin theme={null}
import io.portalhq.android.api.data.noah.NoahComparisonOperator
import io.portalhq.android.api.data.noah.NoahSingleOnchainDepositSourceTriggerAmountCondition
import io.portalhq.android.api.data.noah.NoahSingleOnchainDepositSourceTriggerCondition
import io.portalhq.android.api.data.noah.NoahSingleOnchainDepositSourceTriggerInput

val trigger = NoahSingleOnchainDepositSourceTriggerInput(
  conditions = listOf(
    NoahSingleOnchainDepositSourceTriggerCondition(
      amountConditions = listOf(
        NoahSingleOnchainDepositSourceTriggerAmountCondition(
          comparisonOperator = NoahComparisonOperator.EQ,
          value = quote.cryptoAuthorizedAmount,
        )
      ),
      network = NoahNetwork.SOLANA_DEVNET,
    )
  ),
  sourceAddress = "SoLAddr1111111111111111111111111111111111111",
  expiry = expiry,
  nonce = nonce,
)
```

Trigger inputs serialize their fields in PascalCase (`Type`, `Conditions`, `SourceAddress`, `Expiry`, `Nonce`) to match the Noah API wire format. The same applies to `NoahBusinessFee`, whose fields serialize as `FeeBase`, `FeePct`, and `FiatCurrency` — visible in any logged request body.

<Note>
  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 the saved payment methods available to the customer for payouts, including a pagination token when more results exist.

```kotlin theme={null}
import io.portalhq.android.api.data.noah.NoahGetPaymentMethodsRequest
import io.portalhq.android.api.data.noah.NoahPaymentMethodCapability

portal.ramps.noah.getPaymentMethods(
  NoahGetPaymentMethodsRequest(
    pageSize = 10,
    capability = NoahPaymentMethodCapability.PAYOUT_TO,
  )
).onSuccess { response ->
  response.data.paymentMethods.forEach { method ->
    Log.d("Noah", "${method.id} ${method.paymentMethodCategory} ${method.displayDetails.last4}")
  }
}.onFailure { error ->
  Log.e("Noah", "getPaymentMethods failed", error)
}
```

The `request` parameter is defaulted, so you can call it with no arguments to use server-side defaults:

```kotlin theme={null}
portal.ramps.noah.getPaymentMethods()
  .onSuccess { response -> Log.d("Noah", "${response.data.paymentMethods.size}") }
```

**Signature**

```kotlin theme={null}
suspend fun getPaymentMethods(
  request: NoahGetPaymentMethodsRequest = NoahGetPaymentMethodsRequest(),
): Result<NoahGetPaymentMethodsResponse>
```

| Parameter            | Type                           | Required | Description                                                                                                                                      |
| -------------------- | ------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `request.pageSize`   | `Int?`                         | No       | Maximum number of payment methods to return. The range 1–100 is validated server-side; out-of-range values are rejected with a 400, not clamped. |
| `request.pageToken`  | `String?`                      | No       | Cursor from a previous response's `data.pageToken`.                                                                                              |
| `request.capability` | `NoahPaymentMethodCapability?` | No       | Filter by capability — `PAYOUT_FROM`, `PAYIN_TO`, or `PAYOUT_TO`.                                                                                |

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

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

## Error handling

Every Noah method returns `Result<T>`. Failures are captured inside the `Result` rather than thrown, so handle them with `onFailure`, `fold`, or `getOrElse` — not `try/catch`.

The following exceptions may appear in a failed `Result`:

| Exception                               | Description                                                                                                                                                                                                                                                                                         |
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PortalException.Api.HttpBadRequest`    | HTTP 400. The request was invalid — an unsupported `network`, a malformed amount, an out-of-range `pageSize`, missing required form fields, or `simulatePayin` called against a production environment. Carries `rawBody` with the raw 400 response for parsing structured backend error envelopes. |
| `PortalException.Api.HttpUnauthorized`  | HTTP 401. Verify your Portal client API key and that Noah is enabled for your environment.                                                                                                                                                                                                          |
| `PortalException.Api.HttpRequestFailed` | Any other non-success status — 4xx from 402 upward, and all 5xx. Carries a nullable `statusCode` you can branch on, for example `502` or `503` when Noah is unreachable upstream.                                                                                                                   |
| `java.io.IOException`                   | Network or transport failure. No HTTP response was received.                                                                                                                                                                                                                                        |

<Warning>
  `HttpBadRequest` is a **sibling** of `HttpRequestFailed` under `PortalException.Api`, not a subclass. A branch that only matches `HttpRequestFailed` will **not** catch a 400. Match both explicitly, or match the `PortalException.Api` umbrella.
</Warning>

`HttpRequestFailed.statusCode` is `null` when no valid HTTP response was received, or for synthesized failures such as a `200` body that carried an error envelope. A `null` `statusCode` therefore means "no status to branch on", not "unknown error".

```kotlin theme={null}
import io.portalhq.android.exceptions.PortalException

portal.ramps.noah.getPayoutCountries()
  .onSuccess { response -> Log.d("Noah", "${response.data.countries}") }
  .onFailure { error ->
    when (error) {
      is PortalException.Api.HttpUnauthorized ->
        Log.e("Noah", "Check your Portal client API key and Noah configuration")
      is PortalException.Api.HttpBadRequest ->
        Log.e("Noah", "Invalid request", error)
      is PortalException.Api.HttpRequestFailed ->
        Log.e("Noah", "HTTP ${error.statusCode}", error)
      else ->
        Log.e("Noah", "Transport failure", error)
    }
  }
```

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.

## Related documentation

* [Android SDK API reference](https://portal-hq.github.io) — generated KDoc for `io.portalhq.android.ramps.noah`
* [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/)
