> For the complete documentation index, see [llms.txt](https://docs-sdk.crypto-chief.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs-sdk.crypto-chief.com/processing/php/guides/payouts.md).

# Send a payout

Send single crypto payouts in PHP — estimate, execute idempotently, and wait for confirmation.

A payout sends crypto from your project balance to any address. The flow is **estimate** (optional) → **execute** → **wait for confirmation** (optional).

## Estimate

Preview the network fee and the net amount the recipient receives before sending:

```php
use CryptoChief\Processing\Chain;
use CryptoChief\Processing\Dto\EstimatePayoutRequest;

$est = $client->payouts()->estimate(new EstimatePayoutRequest(
    network:   Chain::EthMainnet->value,
    coin:      'USDT',
    amount:    '25.0',
    toAddress: '0xRecipient...',
));
// $est->amountToReceive, $est->feeInfo->estimatedFiat, $est->sources
```

## Execute

```php
use CryptoChief\Processing\Dto\ExecutePayoutRequest;

$payout = $client->payouts()->execute(new ExecutePayoutRequest(
    network:     Chain::EthMainnet->value,
    coin:        'USDT',
    amount:      '25.0',
    toAddress:   '0xRecipient...',
    orderId:     'order-42',   // idempotency key — safe to retry
    userId:      'u-7',
    urlCallback: 'https://your.app/webhooks/payout',
));
```

{% hint style="info" %}
`orderId` is an **idempotency key**: re-submitting the same `orderId` returns the same payout instead of creating a second one. That is what makes the SDK's automatic retries safe.
{% endhint %}

## Wait for confirmation

```php
$final = $client->payouts()->waitFor($payout->uuid);
if ($final->status === 'paid') {
    foreach ($final->sources ?? [] as $s) {
        echo "paid: tx = {$s->txid}\n";
    }
}
```

Or react to the `payout.*` [webhook](/processing/php/guides/webhooks.md) instead of polling.

### Confirmations

A payout stays `confirm_check` until the transaction of every source reaches `requiredConfirmations`. Then it becomes `paid` and `payout.paid` is sent. The default `waitFor()` timeout is 90 minutes. A `PollTimeoutException` is not a failure: read `lastState->status` and do not resubmit the payout.

```php
$p = $client->payouts()->info($payout->uuid);
if ($p->confirmations !== null) {
    echo "{$p->status}: {$p->confirmations} of {$p->requiredConfirmations}\n";
}
foreach ($p->sources ?? [] as $src) {
    echo $src->address, ': ', $src->confirmations ?? 'not on chain yet', "\n";
}
```

| Field                                  | Type                | Meaning                                                                                                                                                                                                                                                |
| -------------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `sources[]->confirmations`             | `?int`              | Confirmations of the source's transaction; `null` until it is on chain.                                                                                                                                                                                |
| `serviceOperations[]['confirmations']` | `int`, key optional | Confirmations of a service transaction, such as a gas top-up, recorded once when it is included in a block; not updated afterwards. Read it as `$op['confirmations'] ?? null`. The payout waits for the top-up to be included, not for finality depth. |
| `confirmations`                        | `?int`              | The lowest count among the sources; `null` while no source has a transaction.                                                                                                                                                                          |
| `requiredConfirmations`                | `?int`              | The network's finality depth, at least `1`.                                                                                                                                                                                                            |

Compare the counts with `!== null`, not `empty()`: `0` is a real count. The first `execute()` response has no counts yet. Decide on `status`, not on the counts. Withdrawals follow the same rule — see [Manual withdrawals](/processing/php/guides/withdrawals.md#confirmations).

## Pay out in a different asset (swap)

{% hint style="warning" %}
**Not available yet.** The payout request carries an `auto_convert` field and this SDK exposes it, but the platform refuses any payout that sets it, answering `AUTO_CONVERT_NOT_IMPLEMENTED`. `auto_convert_policy` is reserved alongside it. Convert the asset yourself and pay out what the master wallet already holds.
{% endhint %}

## Handle errors

```php
use CryptoChief\Processing\ErrorCode;
use CryptoChief\Processing\Exception\ApiException;

try {
    $client->payouts()->execute($req);
} catch (ApiException $err) {
    if ($err->errorCode === ErrorCode::InsufficientFunds->value) {
        // top up and retry
    } elseif (in_array($err->errorCode, [
        ErrorCode::AssetNotEnabled->value,
        ErrorCode::DebtLimitExceeded->value,
    ], true)) {
        // surface to the user
    } else {
        throw $err;
    }
}
```

See [Errors & retries](/processing/php/concepts/errors.md).

{% content-ref url="/pages/N5Dn75oPI1qDRxghkYml" %}
[Mass payouts](/processing/php/guides/mass-payouts.md)
{% endcontent-ref %}
