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

> Move crypto out of your client's Portal wallet with Due payouts: quote a transfer, deposit onchain, and settle to a bank account.

## Overview

A Due payout is the mirror of a payin: crypto leaves your client's Portal wallet and Due
settles the equivalent fiat to a bank beneficiary. You discover a channel, create the fiat
recipient, quote the transfer, create it, then send crypto to the onchain deposit address
Due returns.

<Note>
  Portal proxies these endpoints to Due and returns Due's responses verbatim. Example
  responses below are illustrative; exact fields come from Due and may evolve.
</Note>

## 1. Prerequisite: approved KYC

Transfers require a provisioned Due customer with approved KYC. Some channels additionally
require endorsements. See [KYC onboarding](/integrations/On-Off-Ramp/due-kyc).

## 2. Discover a channel

`GET /channels` lists the rails and currencies available to the customer. Pass
`onlyAvailable=true` to filter to channels the customer can use today. Pick a crypto source
rail (for example `base-sepolia` / `USDC`) and a fiat destination rail and currency (for
example `sepa` / `EUR`). Each channel also exposes `purposeCodes` and `memoConfig`, which you
will need when creating the transfer.

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

## 3. Create the fiat recipient

The recipient is the bank beneficiary that receives the fiat. Call `POST /recipients` with
`isExternal: true`. Unlike a payin recipient, this is a bank account, not a wallet, so the
CAIP-2 `chainId` defaulting does not apply.

```bash theme={null}
curl --request POST \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/due/recipients \
  --header 'Authorization: Bearer [token]' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: [unique-key]' \
  --data '{
    "name": "Jane Doe",
    "isExternal": true,
    "details": {
      "schema": "bank_sepa",
      "iban": "DE89370400440532013000",
      "accountType": "individual"
    }
  }'
```

* Set `isExternal: true` so Portal forwards the recipient to Due without attempting any
  Portal-wallet address defaulting.
* The exact `details` fields depend on the channel's bank schema (for example `bank_sepa` or
  `bank_us`): account or IBAN, beneficiary address, and entity type. Due validates them and
  returns field-level errors for anything missing.
* The `Idempotency-Key` header is optional and deduplicates retries.

Example response:

```json theme={null}
{
  "data": {
    "id": "rcp_456",
    "isExternal": true,
    "isValid": true
  }
}
```

## 4. Quote the transfer

`POST /transfers/quote` prices the transfer. `source` is the crypto side and `destination`
is the fiat side. Set the destination `amount` to `"0"` to let Due compute the fiat the
beneficiary receives from the crypto amount sent.

```bash theme={null}
curl --request POST \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/due/transfers/quote \
  --header 'Authorization: Bearer [token]' \
  --header 'Content-Type: application/json' \
  --data '{
    "source": { "rail": "base-sepolia", "currency": "USDC", "amount": "100" },
    "destination": { "rail": "sepa", "currency": "EUR", "amount": "0" }
  }'
```

<Note>
  Application fees are enforced by the custodian's Portal Dashboard configuration and injected
  server-side. Any `applicationFeeBps` or `applicationFeeAmount` you send is stripped, so
  clients cannot override them. See [Application fees](/integrations/On-Off-Ramp/due-fees).
</Note>

Example response:

```json theme={null}
{
  "data": {
    "token": "qte_def",
    "fxRate": 0.92,
    "fxMarkup": 0,
    "expiresAt": "2026-07-06T18:05:00Z",
    "source": { "rail": "base-sepolia", "currency": "USDC", "amount": "100", "fee": "0.50", "totalFee": "0.50" },
    "destination": { "rail": "sepa", "currency": "EUR", "amount": "91.54", "fee": "0", "totalFee": "0" }
  }
}
```

## 5. Create the transfer

`POST /transfers` turns a quote into a transfer. Pass the quote `token` and the recipient
`id`.

```bash theme={null}
curl --request POST \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/due/transfers \
  --header 'Authorization: Bearer [token]' \
  --header 'Content-Type: application/json' \
  --header 'Idempotency-Key: [unique-key]' \
  --data '{
    "quote": "qte_def",
    "recipient": "rcp_456",
    "purposeCode": "GDDS"
  }'
```

* `memo` and `purposeCode` are optional. Whether they are required, and which values are
  valid, comes from the channel's `memoConfig` and `purposeCodes` from step 2.
* The `Idempotency-Key` header is optional and deduplicates retries.

Example response:

```json theme={null}
{
  "data": {
    "id": "trf_abc",
    "status": "awaiting_funds",
    "expiresAt": "2026-07-06T18:10:00Z"
  }
}
```

## 6. Get the deposit address

While the transfer is `awaiting_funds`, call `POST /transfers/{transferId}/funding-address`
to get the onchain address to send the source crypto to.

```bash theme={null}
curl --request POST \
  --url https://api.portalhq.io/api/v3/clients/me/integrations/due/transfers/{transferId}/funding-address \
  --header 'Authorization: Bearer [token]'
```

Example response:

```json theme={null}
{
  "data": {
    "kind": "onchain_address",
    "details": {
      "address": "0xc118ad00663be7d0360b56a4e9fbb20530bf4693",
      "network": "base-sepolia"
    }
  }
}
```

<Note>
  For some rails Due returns a `kind` of `external_action`, meaning the deposit needs an
  onchain signature rather than a plain transfer. Read `details` for the specific action.
</Note>

## 7. Send crypto and track settlement

Send the source crypto from your client's Portal wallet to the deposit `details.address`
(for example with the SDK's `sendAsset`). Once the deposit lands, subscribe to
[webhooks](/integrations/On-Off-Ramp/due-webhooks) to track settlement. The status progresses
`awaiting_funds` -> `funds_received` -> `approved` -> `payment_submitted` ->
`payment_processed`.

`GET /transfers/{transferId}` returns a transfer's current state whenever you need to read it
directly.

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

<Tip>
  Acknowledge webhook deliveries quickly with a `2XX`, then process events asynchronously.
</Tip>

## Next steps

* [Payins](/integrations/On-Off-Ramp/due-payins)
* [Virtual accounts](/integrations/On-Off-Ramp/due-virtual-accounts)
* [Webhooks](/integrations/On-Off-Ramp/due-webhooks)
