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

# Depósito de payout assinado pela Astron Pay

> Use POST /payout/build-transfer-tx + POST /merchant/wallet/sign-and-send-transaction para depositar USDC em payouts sem montar transações Solana no seu lado e sem ter SOL para gas.

Esta é a forma **recomendada** de executar o passo de depósito on-chain de uma payout
quando seu merchant utiliza wallet **gerenciada** pela Astron Pay (criada via API ou
no dashboard).

Você nunca toca em `@solana/web3.js`, não precisa de RPC Solana próprio, não precisa
ter SOL para gas, e fica imune aos modos comuns de falha silenciosa (blockhash
expirado, encoding ATA errado, falta de `paymentReference`).

<Note>
  **Quando usar este fluxo.** Use sempre que sua wallet de merchant for gerenciada
  pela Astron Pay. Se você operar self-custody (chave privada do seu lado) e quiser
  pagar o próprio gas, monte a tx localmente — a rota `build-transfer-tx` ainda
  ajuda: ela já devolve a tx pronta com `paymentReference` correto, basta você
  trocar o `feePayer` por sua própria wallet e assinar com sua chave.
</Note>

## Visão geral em 3 passos

1. **Criar a payout** com `POST /payout` — recebe `orderId`, `depositAddress`,
   `paymentReference`.
2. **Pedir a tx pronta** com `POST /payout/build-transfer-tx` — recebe a transação
   serializada em base64, com blockhash recente, ATA do destinatário correto,
   `paymentReference` anexado, e `feePayer = wallet do receiver da order`
   (default).
3. **Assinar e transmitir** com `POST /merchant/wallet/sign-and-send-transaction` —
   a Astron Pay assina pela wallet do receiver, substitui o pagamento de gas pelo
   sponsor da plataforma, e transmite. A resposta só retorna depois que a rede
   confirmou — sem hash fantasma.

## Você não precisa especificar a wallet de origem

A wallet que paga o USDC é deduzida automaticamente da cadeia
`receiverId → quoteId → orderId`:

| # | Chamada                                           | Body                                        | Carrega receiverId?  |
| - | ------------------------------------------------- | ------------------------------------------- | -------------------- |
| 1 | `POST /quote/payout`                              | `{ receiverId, amountCrypto, targetToken }` | ✅ direto             |
| 2 | `POST /payout`                                    | `{ quoteId, ... }`                          | ✅ via quote          |
| 3 | `POST /payout/build-transfer-tx`                  | `{ orderId }`                               | ✅ via order          |
| 4 | `POST /merchant/wallet/sign-and-send-transaction` | `{ transaction }`                           | (a tx já vem pronta) |

Em **nenhuma** das chamadas você passa `fromAddress`, `receiverWalletAddress`
ou similar. A Astron Pay resolve sempre para a **wallet gerenciada do
receiver da order** — montada na criação do receiver com
`createPrivyWallet=true`.

<Warning>
  **Importante:** cada merchant tem múltiplos receivers. Toda chamada que
  mexe em saldo, payout, KYC ou wallet exige que você diga qual receiver
  está agindo (`receiverId` em /quote/payout e /payout, `:id` no path para
  endpoints `/receivers/:id/...`). Se você não passar isso, ou passar o
  `merchantId` no lugar do `receiverId`, a chamada erra ou aponta para a
  wallet errada.

  A wallet do **merchant** existe só como tenant identifier; ela nunca
  segura crypto de cliente final. A wallet do **receiver** é a que detém
  o USDC do payout.
</Warning>

## 1. Criar a payout

Já documentado em [Fluxo Payout](/cripto/guides/payout-flow). O importante para esta
guia é guardar o `id` da order retornada — usaremos no passo 2.

```bash theme={null}
curl -X POST https://api.astronpay.co/api/v1/payout \
  -H "x-api-key: $API_KEY" -H "x-api-secret: $API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "receiverId": "rcv_...",
    "quoteId": "qt_...",
    "amountCrypto": 200.00,
    "pixKey": "12345678901",
    "pixKeyType": "CPF",
    "recipientName": "João Silva",
    "recipientDocument": "12345678901"
  }'
```

Resposta (resumida):

```json theme={null}
{
  "id": "ord_01J...",
  "status": "AWAITING_DEPOSIT",
  "depositAddress": "...",
  "paymentReference": "..."
}
```

## 2. Pedir a tx pronta

```bash theme={null}
curl -X POST https://api.astronpay.co/api/v1/payout/build-transfer-tx \
  -H "x-api-key: $API_KEY" -H "x-api-secret: $API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "ord_01J..." }'
```

Resposta:

```json theme={null}
{
  "transaction": "AQAAAA...base64...",
  "blockhash": "EkSnNWid2cvwEVnVx9aBqawnmiCNiDgp3gUdkDPTKN1N",
  "lastValidBlockHeight": 295804123,
  "expiresAt": "2026-04-27T20:00:50.000Z",
  "fromAddress": "DhTfJ1x2VRvFtwUzafEX2nhwgo6F9VvHrfGhyaZfQER7",
  "depositAddress": "...",
  "paymentReference": "...",
  "amountCrypto": 200.00,
  "sourceToken": "USDC"
}
```

<Tip>
  **`expiresAt` é uma janela de segurança**, não o limite duro do Solana. Em
  mainnet, blockhashes vivem \~60-90 s; nós devolvemos `expiresAt` \~50 s no futuro
  para você ter folga de assinar e transmitir. Se você esperar além desse tempo,
  chame `build-transfer-tx` de novo — é seguro repetir, o pedido continua em
  `AWAITING_DEPOSIT`.
</Tip>

### Parâmetros

<ParamField body="orderId" type="string" required>
  UUID da payout retornada por `POST /payout`. A order deve estar em status
  `AWAITING_DEPOSIT` e pertencer ao merchant autenticado. Esse é **o único
  campo do body** — a wallet de origem é deduzida do receiver da order.
</ParamField>

### Erros

| Código                       | Status | Quando acontece                                                                        | O que fazer                                                                                                                    |
| ---------------------------- | ------ | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `NOT_FOUND`                  | 404    | Order não existe ou pertence a outro merchant                                          | Verificar `orderId`. Por design, a Astron Pay não distingue "não existe" de "é de outro merchant" — para não vazar existência. |
| `INVALID_ORDER_TYPE`         | 400    | Você passou um `orderId` de uma **payin**                                              | Use uma order de payout.                                                                                                       |
| `INVALID_ORDER_STATE`        | 409    | A order já avançou para `DEPOSIT_RECEIVED`/`COMPLETED`/`FAILED`/`EXPIRED`/`REFUNDED`   | A order não aceita mais depósito. Crie uma nova se precisar.                                                                   |
| `RECEIVER_WALLET_MISSING`    | 409    | O receiver da order não tem wallet gerenciada                                          | Crie o receiver com `createPrivyWallet=true` em `POST /receivers`.                                                             |
| `RECEIVER_WALLET_NOT_FUNDED` | 422    | A wallet do receiver existe mas não tem USDC (ATA não inicializada)                    | Mande USDC para a wallet do receiver (NÃO a do merchant). A mensagem do erro inclui o endereço exato.                          |
| `INSUFFICIENT_TOKEN_BALANCE` | 422    | A wallet do receiver tem saldo, mas menor que o valor da order                         | Top up a wallet do receiver com a diferença e retente.                                                                         |
| `UNSUPPORTED_TOKEN`          | 400    | Token de origem da order fora da lista suportada (`USDC`, `USDT`, `SOL`)               | Refaça a payout especificando `sourceToken: "USDC"`.                                                                           |
| `VALIDATION_ERROR`           | 400    | `orderId` não é UUID, `fromAddress` não é base58 válido                                | Corrija o body da requisição.                                                                                                  |
| `ORDER_INCONSISTENT`         | 500    | Defesa em profundidade: a row da order está corrompida (ex.: falta `paymentReference`) | Contate o suporte.                                                                                                             |

### O que está dentro do `transaction`

A tx serializada já tem tudo que a rede precisa:

* **Instrução SPL Transfer** transferindo `amountCrypto × 10^decimals` (smallest unit)
  do **ATA do `fromAddress`** para o **ATA do `depositAddress`** no mint
  configurado (`USDC` → `EPjFWdd5...`).
* **`paymentReference`** anexado como conta extra **non-signer, non-writable** na
  instrução. É assim que a Astron Pay correlaciona o depósito on-chain com a sua
  order — se você montar a tx no seu lado e esquecer de incluir, o webhook
  `order.deposit_received` nunca dispara.
* Caso a ATA da plataforma ainda não exista (caso muito raro), a tx começa com a
  instrução `CreateAssociatedTokenAccount`. Sem custo extra para você.
* **`feePayer = fromAddress`**. Necessário para a serialização Solana. Quando a
  tx for transmitida via `/merchant/wallet/sign-and-send-transaction`, o sponsor
  da Astron Pay substitui o pagamento real do SOL.
* **`recentBlockhash`** recente (`finalized` commitment) — o mesmo valor que vem
  no campo `blockhash` da resposta.
* **Sem assinaturas** — tx pronta para ser assinada.

## 3. Assinar e transmitir

Pegue o `transaction` da resposta e mande direto para
`/merchant/wallet/sign-and-send-transaction`:

```bash theme={null}
curl -X POST https://api.astronpay.co/api/v1/merchant/wallet/sign-and-send-transaction \
  -H "x-api-key: $API_KEY" -H "x-api-secret: $API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{ "transaction": "AQAAAA...base64..." }'
```

Resposta:

```json theme={null}
{ "hash": "5sQAcj7...assinatura-on-chain..." }
```

<Warning>
  **Esta resposta só vem depois que a rede confirmou a transação.** Internamente a
  Astron Pay verifica `getSignatureStatuses` antes de retornar 200. Se a transação
  não for confirmada em \~15 s, você recebe `502 TRANSACTION_NOT_BROADCASTED` em
  vez de um falso sucesso. Trate esse 502 como retry-safe: chame
  `build-transfer-tx` de novo (blockhash novo) e reenvie.
</Warning>

## Exemplo end-to-end (TypeScript)

Sem dependências Solana — só `fetch`.

```ts theme={null}
async function executePayoutDeposit(opts: {
  orderId: string;
  apiKey: string;
  apiSecret: string;
  baseUrl?: string;
}): Promise<string> {
  const baseUrl = opts.baseUrl ?? "https://api.astronpay.co/api/v1";
  const headers = {
    "Content-Type": "application/json",
    "x-api-key": opts.apiKey,
    "x-api-secret": opts.apiSecret,
  };

  // 1. Pede a tx pronta para o orderId
  const buildRes = await fetch(`${baseUrl}/payout/build-transfer-tx`, {
    method: "POST",
    headers,
    body: JSON.stringify({ orderId: opts.orderId }),
  });
  if (!buildRes.ok) {
    const err = await buildRes.json();
    throw new Error(`build-transfer-tx failed: ${JSON.stringify(err)}`);
  }
  const { transaction } = (await buildRes.json()) as { transaction: string };

  // 2. Assina e transmite — a Astron Pay paga o gas
  const sendRes = await fetch(
    `${baseUrl}/merchant/wallet/sign-and-send-transaction`,
    {
      method: "POST",
      headers,
      body: JSON.stringify({ transaction }),
    },
  );
  if (!sendRes.ok) {
    const err = await sendRes.json();
    if (err?.error?.code === "TRANSACTION_NOT_BROADCASTED") {
      // Safe to retry: a tx não foi confirmada na rede.
      throw new Error("Network did not confirm. Retry with a fresh build-transfer-tx call.");
    }
    throw new Error(`sign-and-send failed: ${JSON.stringify(err)}`);
  }

  const { hash } = (await sendRes.json()) as { hash: string };
  return hash;
}

// Uso:
const txHash = await executePayoutDeposit({
  orderId: "ord_01J...",
  apiKey: process.env.ASTRON_PAY_API_KEY!,
  apiSecret: process.env.ASTRON_PAY_API_SECRET!,
});
console.log(`https://solscan.io/tx/${txHash}`);
```

## Estratégia de retry

Use a estratégia abaixo como referência. **Não** retente um 4xx — eles indicam
input inválido. **Retente** 502 e 5xx, sempre regerando a tx.

```ts theme={null}
async function executePayoutDepositWithRetry(opts: { orderId: string; apiKey: string; apiSecret: string }) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      return await executePayoutDeposit(opts);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      if (msg.includes("Network did not confirm") && attempt < 3) {
        // Backoff curto antes de tentar de novo. blockhash novo no próximo loop.
        await new Promise((r) => setTimeout(r, 2000 * attempt));
        continue;
      }
      throw err;
    }
  }
  throw new Error("Exhausted retries");
}
```

## Checklist

* [ ] Você cria a payout com `POST /payout` e guarda `id`, `depositAddress`,
  `paymentReference`.
* [ ] Você chama `POST /payout/build-transfer-tx` **logo antes** de assinar
  (não cacheia a tx — blockhash expira em menos de 90 s).
* [ ] Você manda o campo `transaction` retornado **sem alteração** para
  `POST /merchant/wallet/sign-and-send-transaction`.
* [ ] Você trata `502 TRANSACTION_NOT_BROADCASTED` como retry-safe (regerar
  a tx via `build-transfer-tx`).
* [ ] Você assina o webhook `order.deposit_received` para saber quando a
  Astron Pay detectou o depósito on-chain (a correlação é feita pelo
  `paymentReference` que já está dentro da tx que enviamos a você).

## Quando NÃO usar este fluxo

* **Self-custody**: você tem chave privada própria e quer assinar localmente.
  Use `/payout/build-transfer-tx` para gerar a tx, troque o `feePayer` por sua
  wallet, assine com sua chave, e transmita pelo seu RPC. Você paga o próprio
  gas (a Astron Pay só patrocina gas para wallets gerenciadas).
* **Transferências fora do contexto de payout**: a rota `build-transfer-tx`
  serve **apenas** para o passo de depósito de uma payout existente. Para
  qualquer outra movimentação on-chain, monte a tx no seu lado e use
  `/merchant/wallet/sign-and-send-transaction` diretamente.
