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

# Payouts

> Run Noah payout workflows through Portal from channel discovery and form rendering to quote and payout initiation.

## Overview

Noah payouts let your client convert stablecoins to fiat and disburse through local payout rails.

## 1. Prerequisite: approved KYC

Ensure the client has completed Noah onboarding and has `APPROVED` status.

See [Noah KYC onboarding](/integrations/On-Off-Ramp/noah-kyc).

## 2. Get supported payout countries

```bash theme={null}
curl --request GET \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/noah/payouts/countries \
  --header 'Authorization: Bearer [token]'
```

The `countries` map includes many additional country/currency combinations based on Noah support.

## 3. Get payout channels

Call `GET /payouts/channels` with at least `cryptoCurrency`. `country` and
`fiatCurrency` are optional filters; pass either or both to narrow results.

```bash theme={null}
curl --request GET \
  --url 'https://api.portalhq.io/api/v3/clients/me/integrations/noah/payouts/channels?country=BR&cryptoCurrency=USDC_TEST&fiatCurrency=BRL' \
  --header 'Authorization: Bearer [token]'
```

<Note>
  In Noah's sandbox, cryptocurrency tickers must be suffixed with `_TEST` (e.g. `USDC_TEST`). Drop the suffix in production. See [Noah's sandbox naming convention](https://docs.noah.com/products/global-payouts-api/#sandbox-cryptocurrency-naming-convention).
</Note>

<Note>
  Optional query params:

  * `fiatAmount` — pricing context for the returned `calculated.totalFee`.
  * `paymentMethodId` — scope channels to a specific existing payment method.
  * `pageSize` — page size (1-100).
  * `pageToken` — pagination token from a previous response.
</Note>

## 4. (Optional) Get saved payment methods

Call `GET /payouts/payment-methods` to list saved payout destinations for the approved customer.

If you want to use a saved payout destination, you can pass its `id` as `paymentMethodId` in `POST /payouts/quote`.

```bash theme={null}
curl --request GET \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/noah/payouts/payment-methods \
  --header 'Authorization: Bearer [token]'
```

Example response (truncated to key fields):

```json theme={null}
{
  "data": {
    "paymentMethods": [
      {
        "id": "Bank/Ach/USD/021000021/123456789/cmmw6tzqc00003saqzbgwueb5",
        "paymentMethodCategory": "Bank",
        "customerId": "cmmw6tzqc00003saqzbgwueb5",
        "country": "US",
        "capabilities": {
          "payinTo": false,
          "payoutFrom": false,
          "payoutTo": true
        },
        "displayDetails": {
          "type": "FiatPaymentMethodBankDisplay",
          "accountNumber": "123456789",
          "bankCode": "021000021"
        },
        "accountHolderDetails": {
          "name": {
            "firstName": "John",
            "lastName": "Mock-Doe"
          }
        },
        "issuerDetails": {}
      }
    ]
  }
}
```

`pageToken` may also be present when pagination is returned.

## 5. Get dynamic form for selected channel

Use `GET /payouts/channels/:channelId/form` and render the returned `formSchema`.

```bash theme={null}
curl --request GET \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/noah/payouts/channels/[channelId]/form \
  --header 'Authorization: Bearer [token]'
```

Example response (truncated to key fields):

```json theme={null}
{
  "data": {
    "formMetadata": {
      "contentHash": "eda5d74d98e3e7429644fd7f74d4979638eca0e9aa7422091a600d85c1f3422a"
    },
    "formSchema": {
      "$schema": "http://json-schema.org/draft-07/schema#",
      "type": "object",
      "properties": {
        "AccountHolderName": {
          "type": "object",
          "title": "Account Holder Name"
        },
        "AccountHolderAddress": {
          "type": "object",
          "title": "Account Holder Address"
        },
        "BankDetails": {
          "type": "object",
          "title": "Bank Details"
        },
        "PaymentPurpose": {
          "type": "string",
          "title": "Payment Purpose"
        }
      },
      "required": [
        "AccountHolderName",
        "AccountHolderAddress",
        "BankDetails",
        "PaymentPurpose"
      ]
    }
  }
}
```

`formSchema` can include additional nested validation rules (`allOf`, `if`/`then`, `minLength`, `maxLength`, `pattern`) that should be rendered and validated in your form UI.

## 6. Quote payout

Call `POST /payouts/quote` to validate form data and get fee + estimate.
`paymentMethodId` is optional and is only needed when quoting against a saved payment method from step 4.

Provide **exactly one** of `fiatAmount` or `cryptoAmount`:

* `fiatAmount` — amount the recipient receives. The quote derives the equivalent crypto.
* `cryptoAmount` — exact crypto the depositor will send. The quote derives the equivalent fiat (returned in the response's `fiatAmount` disclosure field).

Pass `quoted: true` to request a fixed-rate quote. When `quoted` is true and
all form steps are complete, the response includes a `quote` object with a
`signedQuote` and `expiry` issued by Noah.

Forward `quote.signedQuote` unchanged into the **Quoted trigger** on
`POST /payouts` (as `trigger.SignedQuote`) to lock the rate end-to-end. See
the trigger variants in step 7.

Optional `businessFee` applies a per-transaction business fee on top of the
customer's payout (fixed `FeeBase` and/or `FeePct` in the given `FiatCurrency`).

```bash theme={null}
curl --request POST \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/noah/payouts/quote \
  --header 'Authorization: Bearer [token]' \
  --header 'Content-Type: application/json' \
  --data '{
    "channelId": "214eab50-e22b-5e0f-b487-37dc1addee90",
    "cryptoCurrency": "USDC_TEST",
    "fiatAmount": "0.01",
    "fiatCurrency": "USD",
    "form": {
      "AccountHolderName": {
        "AccountHolderType": "Individual",
        "Name": {
          "FirstName": "John",
          "LastName": "Mock-Doe"
        }
      },
      "AccountHolderAddress": {
        "Address": "123 Main St",
        "City": "Miami",
        "PostalCode": "33139",
        "State": "FL"
      },
      "BankDetails": {
        "AccountNumber": "900711447286",
        "BankCode": "101019644",
        "AccountType": "Checking"
      },
      "PaymentPurpose": "Personal transfer",
      "Reference": "Optional reference"
    }
  }'
```

Example response (truncated to key fields):

```json theme={null}
{
  "data": {
    "payoutId": "cmmyatsdk0000wy2en3lof5jj",
    "formSessionId": "d5f81409-5306-476b-84d6-6be2b5656a5a",
    "cryptoAmountEstimate": "0.010004",
    "cryptoAuthorizedAmount": "0.010004",
    "totalFee": "0.01",
    "cryptoCurrency": "USDC_TEST",
    "fiatCurrency": "USD",
    "fiatAmount": "0.01",
    "rate": "0.999",
    "breakdown": [
      { "type": "ChannelFee", "amount": "0.000004" },
      { "type": "BusinessFee", "amount": "0" },
      { "type": "Remaining", "amount": "0.01" }
    ]
  }
}
```

When `quoted: true` was sent in the request and all form steps are complete,
the response also includes a `quote` object:

```json theme={null}
{
  "data": {
    "quote": {
      "signedQuote": "eyJhbGciOi...",
      "expiry": "2026-03-24T08:00:55Z"
    }
  }
}
```

To lock the rate, pass `quote.signedQuote` unchanged on `POST /payouts`
inside the Quoted trigger as `trigger.SignedQuote`. The deposit must arrive
and finalize before `quote.expiry`; otherwise the workflow proceeds at the
market rate at execution time.

## 7. Initiate payout

Call `POST /payouts` using `payoutId` returned from quote.

`nonce` must be unique per transaction attempt (max 36 characters). Reuse the same nonce only when retrying the same payout request for idempotency.

```bash theme={null}
curl --request POST \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/noah/payouts \
  --header 'Authorization: Bearer [token]' \
  --header 'Content-Type: application/json' \
  --data '{
    "payoutId":"cmmyatsdk0000wy2en3lof5jj",
    "sourceAddress":"DEeUjZtzWWiM6sJPBFsR5iL9AeSEtvtuh4ycHM4LHyTK",
    "expiry":"2026-03-24T23:59:59Z",
    "nonce":"nonce-20260320-02388933",
    "network":"solana:devnet"
  }'
```

Example response:

```json theme={null}
{
	"data": {
		"destinationAddress": "4dguFohaa8F9MvRtZecGsHCvnGqzYPyGyusP6BWu4zLK",
		"conditions": [
			{
				"amountConditions": [
					{
						"comparisonOperator": "EQ",
						"value": "0.010004"
					}
				],
				"cryptoCurrency": "USDC_TEST",
				"destinationAddress": "4dguFohaa8F9MvRtZecGsHCvnGqzYPyGyusP6BWu4zLK",
				"network": "SolanaDevnet"
			}
		]
	}
}
```

<Note>
  After receiving this response, submit the onchain transfer to the returned `destinationAddress` and ensure it satisfies the returned `conditions` (network and amount constraints).
</Note>

### Trigger variants

You can pass an explicit `trigger` to control how the sell rule fires. The
`Type` field selects the variant:

* **`SingleOnchainDepositSourceTriggerInput`** *(default)* — executes once.
  `Conditions[*]` must carry `AmountConditions`. Portal forwards the persisted
  `fiatAmount` from the quote when set.
* **`PermanentOnchainDepositSourceTriggerInput`** — fires on every matching
  deposit; the entire deposited crypto amount is always sold. `Conditions[*]`
  carries `Network` only — **omit** `AmountConditions`. Optional
  `NetworkAgnostic: true` matches deposits on any network.
* **`QuotedOnchainDepositSourceTriggerInput`** — executes once and locks the
  conversion rate from a prior `SellQuote`. Requires `SignedQuote` on the
  trigger (from `quote.signedQuote` returned by `POST /payouts/quote`).
  `Conditions[*]` carries `Network` only — **omit** `AmountConditions`.
  `Expiry` is optional and defaults to the bound quote's expiry.

**Permanent trigger** (no `AmountConditions`):

```json theme={null}
{
  "trigger": {
    "Type": "PermanentOnchainDepositSourceTriggerInput",
    "Conditions": [{ "Network": "SolanaDevnet" }],
    "SourceAddress": "DEeUjZtzWWiM6sJPBFsR5iL9AeSEtvtuh4ycHM4LHyTK",
    "Expiry": "2026-03-24T23:59:59Z",
    "Nonce": "nonce-20260320-02388933",
    "NetworkAgnostic": false
  }
}
```

**Quoted trigger** (carry the `signedQuote` returned by `POST /payouts/quote`
as `trigger.SignedQuote`; no `AmountConditions`; `Expiry` defaults to the
bound quote's expiry):

```json theme={null}
{
  "trigger": {
    "Type": "QuotedOnchainDepositSourceTriggerInput",
    "SignedQuote": "eyJhbGciOi...",
    "Conditions": [{ "Network": "SolanaDevnet" }],
    "SourceAddress": "DEeUjZtzWWiM6sJPBFsR5iL9AeSEtvtuh4ycHM4LHyTK",
    "Nonce": "nonce-20260320-02388933"
  }
}
```

`ComparisonOperator` values for the Single trigger: `EQ`, `LTEQ`, `GTEQ`.

You can also pass an optional `businessFee` at the top level of the payout
request — same shape as on `POST /payouts/quote`.

## 8. Configure Noah webhooks for payout updates

Set up Noah webhooks in Noah Dashboard to receive payout lifecycle and transaction updates directly from Noah.

* Subscribe to `Transaction` events for payout execution and completion updates.
* [Noah Webhooks](/integrations/On-Off-Ramp/noah-webhooks)

## 9. Monitor payout progress

Use `Transaction` webhook events and Noah Dashboard delivery logs to track payout execution and reconciliation.

<Warning>
  `POST /payouts` only works for payout intents in `PREPARED` status (created by `POST /payouts/quote`).
</Warning>
