# Sending Payments (/docs/guides/wallets/sending)

- [Sample: send a payment](https://github.com/Shiga-Digital/Shiga---wallet-infrastructure-quickstart/tree/main/02-send-payment): Quote, prepare, passkey approval, submit and wait for confirmation, on EVM and other networks.

Payment flow: Your server → Prepare payment → Passkey approves → Submit, then Recipient or Your server.

## 1. Check the balance [#1-check-the-balance]

[List wallet balances](/docs/api-reference/balances/list-wallet-balances) returns every wallet of a customer with its balances.

## 2. Get a quote [#2-get-a-quote]

[Get fee quote](/docs/api-reference/quotes/get-fee-quote) returns the network fee and the maximum amount the wallet can send. Do not send the sender's address: we find the wallet from `moduleId` and the `x-customer-id` header, and return the one we priced.

Amounts are in the asset's display unit, for example `"12.5"` USD₮ or `"0.001"` BTC, never in base units.

## 3. Send [#3-send]

Every payment is approved with the customer's passkey. The flow depends on the network.

### EVM (`evm_erc4337`) [#evm-evm_erc4337]

1. [Prepare userOp](/docs/api-reference/transactions/prepare-userop) with the chain, token, recipient and amount. It returns a `userOpHash`.
2. Have the customer sign the `userOpHash` with their passkey.
3. [Submit userOp signature](/docs/api-reference/transactions/submit-userop-signature). The payment is broadcast once the wallet's signing threshold is met; for most wallets that is one signature.

#### Example: approving an EVM payment [#example-approving-an-evm-payment]

In the browser, turn the `userOpHash` into the passkey challenge and ask the customer to approve:

```ts
import { startAuthentication } from '@simplewebauthn/browser';

function hashToChallenge(userOpHash: string) {
  const bytes = userOpHash
    .slice(2)
    .match(/../g)!
    .map(h => parseInt(h, 16));
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

const challenge = hashToChallenge(userOpHash);
const assertion = await startAuthentication({
  optionsJSON: {
    challenge,
    rpId: 'your-domain.com',
    allowCredentials: [{ id: credentialId, type: 'public-key' }],
    userVerification: 'required',
  },
});
await fetch('/payments/approve', {
  method: 'POST',
  body: JSON.stringify({ userOpHash, challenge, assertion }),
});
```

On your server, submit the signature:

```ts
await fetch(`https://api.pilot.shiga.io/v1/userops/${userOpHash}/signatures`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.SHIGA_API_KEY}`,
    'x-customer-id': customerId,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    passkeyPublicKey: { x: passkey.x, y: passkey.y },
    passkeyData: {
      credential: assertion,
      expectedChallenge: challenge,
      expectedOrigins: ['https://your-domain.com'],
      expectedRPID: 'your-domain.com',
      authenticator: {
        credentialId: passkey.credentialId,
        credentialPublicKey: passkey.credentialPublicKey,
        counter: 0,
      },
    },
  }),
});
```

`passkeyPublicKey` is the `{ x, y }` returned when the wallets were provisioned. `expectedChallenge` must be the base64url encoding of the `userOpHash` bytes, not the hash as text.

### Bitcoin, Spark, Solana, Tron and TON [#bitcoin-spark-solana-tron-and-ton]

On these networks the wallet key is held in our signing enclave. The enclave signs only after it verifies the customer's passkey approval of the exact transfer. See [How wallets are secured](/docs/guides/wallets/creating-wallets#how-wallets-are-secured).

1. **Choose a `transactionId`.** Your own integer sequence number for this transfer. Keep it the same if you retry.
2. **Build the challenge** from the wallet's `entityKey`, its `moduleId` and the transfer details (below).
3. **Have the customer approve** with their passkey, using the challenge.
4. **Submit** with [Submit transaction](/docs/api-reference/transactions/submit-transaction). We sign and broadcast in one step.

#### Transfer details (`txParams`) [#transfer-details-txparams]

| Field              | Value                                                                                                |
| ------------------ | ---------------------------------------------------------------------------------------------------- |
| `chain`            | The network, for example `tron`.                                                                     |
| `amount`           | In the asset's display unit, for example `"12.5"` USD₮ or `"0.0001"` BTC.                            |
| `recipientAddress` | The destination, in the network's own address format.                                                |
| `transactionId`    | Your integer sequence number from step 1.                                                            |
| `networkConfig`    | `{}`, or `{ "tokenMint": "..." }` on Solana and `{ "tokenRoot": "..." }` on TON to choose the token. |
| `selectedPayment`  | `"native"`. On Solana the platform sets it. It is not part of the challenge.                         |

#### Building the challenge [#building-the-challenge]

The challenge is the base64url SHA-256 of a canonical JSON object: keys sorted at every level, no spaces.

| Key         | Value                                                                                                  |
| ----------- | ------------------------------------------------------------------------------------------------------ |
| `entityKey` | The wallet's `entityKey` from [List wallets](/docs/api-reference/wallets/list-wallets).                |
| `moduleId`  | For example `tron`.                                                                                    |
| `txParams`  | Your `txParams` without `selectedPayment`, and with `networkConfig` cut to `tokenMint` or `tokenRoot`. |

```ts
import { createHash } from 'node:crypto';

function canonical(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(canonical);
  if (value && typeof value === 'object') {
    return Object.fromEntries(
      Object.keys(value)
        .sort()
        .map(key => [key, canonical((value as Record<string, unknown>)[key])]),
    );
  }
  return value;
}

function transferChallenge(
  entityKey: string,
  moduleId: string,
  txParams: Record<string, unknown>,
) {
  const { selectedPayment, networkConfig, ...rest } = txParams;
  const asset = Object.fromEntries(
    Object.entries((networkConfig ?? {}) as Record<string, unknown>).filter(
      ([key]) => ['tokenMint', 'tokenRoot'].includes(key),
    ),
  );
  const intent =
    networkConfig === undefined ? rest : { ...rest, networkConfig: asset };
  return createHash('sha256')
    .update(
      JSON.stringify(canonical({ entityKey, moduleId, txParams: intent })),
    )
    .digest('base64url');
}
```

#### Submitting [#submitting]

Send `moduleId`, `txParams` and `passkeyData`: the customer's assertion (`credential`), the challenge as `expectedChallenge`, and the passkey's `authenticator` details. The response returns the transaction with status `submitted`.

On a treasury wallet that needs more than one approval, each signer submits the same transfer with their own passkey. Until enough have approved, the response carries `awaitingApprovals`; approvals lapse after 15 minutes.

A payment to the wallet's own address is refused with `400 validation_failed`.

## Payment statuses and events [#payment-statuses-and-events]

| Status      | Event                   | Credit or settle? |
| ----------- | ----------------------- | ----------------- |
| `submitted` | `transaction.submitted` | No                |
| `pending`   | `transaction.pending`   | No                |
| `confirmed` | `transaction.success`   | Yes               |
| `failed`    | `transaction.failed`    | No                |

`transaction.reconciled` is the signing side's report of the same confirmation as `transaction.success`; act on `transaction.success`. `transaction.summary_corrected` corrects a past transaction's details and does not change its status.

Only `confirmed` means paid; see [Reliability](/docs/guides/platform/reliability). Read payments with [List transactions](/docs/api-reference/transactions/list-transactions), or receive each change as a notification. See [Notifications](/docs/guides/notifications).

## Errors [#errors]

| Code                         | Status        | Cause                                                                                                                                       |
| ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `validation_failed`          | 400           | Malformed body, an address that fails its network's checksum, an asset the network does not list, or a payment to the wallet's own address. |
| `insufficient_funds`         | 400           | The wallet cannot cover the amount and fees.                                                                                                |
| `wallet_not_found`           | 404           | No single wallet matched `moduleId` and `x-customer-id`. Send `x-customer-id`.                                                              |
| `chain_unavailable`          | 409           | The wallet is not yet deployed on this network; retry once it is.                                                                           |
| `transfer_reverted`          | 409           | The network would reject the transfer.                                                                                                      |
| `passkey_credential_invalid` | 409           | The passkey assertion did not verify.                                                                                                       |
| `paymaster_rejected`         | 502           | The network fee could not be sponsored; retry.                                                                                              |
| `upstream_unavailable`       | 502, 503, 504 | A network service did not answer in time; retry.                                                                                            |