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

# Self-Custody Wallet Integration

> Send and receive cryptocurrency to and from user-controlled wallets

export const FeatureCardGrid = ({cols = 3, children}) => <div className={`not-prose feature-cards-grid feature-cards-cols-${cols}`}>
    {children}
  </div>;

export const FeatureCard = ({icon, title, children, href, linkHref, linkText, color, tag, tagPosition, layout, variant, iconSize}) => {
  const isHorizontal = layout === 'horizontal';
  const isFlat = variant === 'flat';
  const isLargeIcon = iconSize === 'lg';
  const isInlineTag = tagPosition === 'inline';
  const card = <div className={`feature-card ${href ? 'feature-card-link' : ''} ${!icon ? 'feature-card-no-icon' : ''} ${isHorizontal ? 'feature-card-horizontal' : ''} ${isFlat ? 'feature-card-flat' : ''} ${isLargeIcon ? 'feature-card-icon-lg' : ''}`}>
      {icon && <div className="feature-card-icon-wrapper">
          {color ? <div className="feature-card-icon" style={{
    WebkitMaskImage: `url(${icon})`,
    maskImage: `url(${icon})`,
    backgroundColor: color,
    width: '24px',
    height: '24px',
    WebkitMaskSize: 'contain',
    maskSize: 'contain',
    WebkitMaskRepeat: 'no-repeat',
    maskRepeat: 'no-repeat'
  }} /> : <img src={icon} alt="" className="feature-card-icon" />}
        </div>}
      <div className="feature-card-content">
        {isInlineTag ? <div className="feature-card-title-row">
            <span className="feature-card-title">{title}</span>
            {tag && <span className="feature-card-tag">{tag}</span>}
          </div> : <div className="feature-card-title">{title}</div>}
        <div className="feature-card-desc">{children}</div>
        {tag && !isInlineTag && <div className="feature-card-tag-row"><span className="feature-card-tag">{tag}</span></div>}
        {linkText && <div className="feature-card-link-row">
            {linkHref ? <a href={linkHref} className="feature-card-text-link" style={{
    color: color
  }}>
                {linkText}
              </a> : <span className="feature-card-text-link feature-card-coming-soon" style={{
    color: color,
    opacity: 0.6
  }}>
                {linkText}
              </span>}
          </div>}
      </div>
    </div>;
  return href ? <a href={href} className="feature-card-anchor">{card}</a> : card;
};

## Overview

Grid supports sending Bitcoin via Lightning Network to self-custody wallets using Spark wallet addresses. This enables users to maintain full control of their crypto while benefiting from Grid's fiat-to-crypto conversion and payment rails.

<Info>
  Spark wallets use the Lightning Network for instant, low-cost Bitcoin
  transactions. Users can receive payments directly to their self-custody
  wallets without Grid holding their funds.
</Info>

## How it works

1. **User provides wallet address** - Get Spark wallet address from user
2. **Create quote** - Generate quote for fiat-to-crypto or crypto-to-crypto transfer
3. **Execute transfer** - Send crypto directly to user's wallet
4. **Instant settlement** - Lightning Network provides near-instant confirmation

## Prerequisites

* Customer created in Grid
* Valid Spark wallet address from user
* Webhook endpoint for payment notifications (optional, for status updates)

## Sending crypto to self-custody wallets

### Step 1: Collect wallet address

Request the user's Spark wallet address. Spark addresses start with `spark1`:

```javascript theme={null}
function validateSparkAddress(address) {
  // Spark addresses start with spark1 and are typically 90+ characters
  if (!address.startsWith("spark1")) {
    throw new Error("Invalid Spark wallet address");
  }

  if (address.length < 90) {
    throw new Error("Spark address appears incomplete");
  }

  return address;
}

// Example valid address
const sparkAddress =
  "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu";
```

<Warning>
  Always validate wallet addresses before creating quotes. Invalid addresses
  will cause transaction failures and potential fund loss.
</Warning>

### Step 2: Create external account for the wallet

Register the Spark wallet as an external account:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
    -H 'Authorization: Basic $GRID_CLIENT_ID:$GRID_CLIENT_SECRET' \
    -H 'Content-Type: application/json' \
    -d '{
      "customerId": "Customer:019542f5-b3e7-1d02-0000-000000000001",
      "currency": "BTC",
      "accountInfo": {
        "accountType": "SPARK_WALLET",
        "address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
      }
    }'
  ```

  ```javascript Node.js theme={null}
  const externalAccount = await fetch(
    "https://api.lightspark.com/grid/2025-10-13/customers/external-accounts",
    {
      method: "POST",
      headers: {
        Authorization: `Basic ${credentials}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
        currency: "BTC",
        accountInfo: {
          accountType: "SPARK_WALLET",
          address: sparkAddress,
        },
      }),
    }
  ).then((r) => r.json());

  console.log("External account ID:", externalAccount.id);
  ```

  ```python Python theme={null}
  import requests
  import base64

  credentials = base64.b64encode(
      f"{api_token_id}:{api_secret}".encode()
  ).decode()

  response = requests.post(
      'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts',
      headers={
          'Authorization': f'Basic {credentials}',
          'Content-Type': 'application/json'
      },
      json={
          'customerId': 'Customer:019542f5-b3e7-1d02-0000-000000000001',
          'currency': 'BTC',
          'accountInfo': {
              'accountType': 'SPARK_WALLET',
              'address': spark_address
          }
      }
  )

  external_account = response.json()
  print(f"External account ID: {external_account['id']}")
  ```
</CodeGroup>

<Tip>
  Store the external account ID for future transfers. Users can reuse the same
  wallet address for multiple transactions.
</Tip>

### Step 3: Create and execute a quote

#### Option A: From fiat to self-custody wallet

Convert fiat directly to crypto in user's wallet:

```javascript theme={null}
// Create quote for fiat-to-crypto
const quote = await fetch("https://api.lightspark.com/grid/2025-10-13/quotes", {
  method: "POST",
  body: JSON.stringify({
    source: {
      sourceType: "REALTIME_FUNDING",
      customerId: "Customer:019542f5-b3e7-1d02-0000-000000000001",
      currency: "USD",
    },
    destination: {
      destinationType: "ACCOUNT",
      accountId: externalAccount.id,
    },
    lockedCurrencySide: "SENDING",
    lockedCurrencyAmount: 10000, // $100.00
    description: "Buy Bitcoin to self-custody wallet",
  }),
}).then((r) => r.json());

// Display payment instructions to user
console.log("Send fiat to:", quote.paymentInstructions);
console.log("Will receive:", `${quote.totalReceivingAmount / 100000000} BTC`);
```

#### Option B: From internal account to self-custody wallet

Transfer crypto from internal account to user's wallet:

```javascript theme={null}
// Same-currency transfer (no quote needed)
const transaction = await fetch(
  "https://api.lightspark.com/grid/2025-10-13/transfer-out",
  {
    method: "POST",
    body: JSON.stringify({
      source: {
        accountId: "InternalAccount:a12dcbd6-dced-4ec4-b756-3c3a9ea3d123",
      },
      destination: {
        accountId: externalAccount.id,
      },
      amount: 100000, // 0.001 BTC in satoshis
    }),
  }
).then((r) => r.json());

console.log("Transfer initiated:", transaction.id);
```

### Step 4: Monitor transfer completion

Track the transfer status via `OUTGOING_PAYMENT.<STATUS>` webhooks:

```javascript theme={null}
app.post("/webhooks/grid", async (req, res) => {
  const { type, data } = req.body;

  switch (type) {
    case "OUTGOING_PAYMENT.COMPLETED":
      await notifyUser(data.customerId, {
        message: "Bitcoin sent to your wallet!",
        amount: `${data.receivedAmount.amount / 100000000} BTC`,
      });
      break;

    case "OUTGOING_PAYMENT.FAILED":
      await notifyUser(data.customerId, {
        message: "Transfer failed",
        reason: data.failureReason,
        action: "Please verify your wallet address",
      });
      // Refund webhook (OUTGOING_PAYMENT.REFUND_*) will follow
      break;

    case "OUTGOING_PAYMENT.REFUND_COMPLETED":
      await notifyUser(data.customerId, {
        message: "Refund completed. Funds returned to your account.",
      });
      break;
  }

  res.status(200).json({ received: true });
});
```

See the [Transaction Lifecycle](/platform-overview/core-concepts/transaction-lifecycle) guide for all status transitions and refund handling.

## Best practices

<AccordionGroup>
  <Accordion title="Validate wallet addresses">
    Always validate Spark addresses before processing:

    ```javascript theme={null}
    function validateSparkAddress(address) {
      // Check format
      if (!address.startsWith("spark1")) {
        return { valid: false, error: "Must start with spark1" };
      }

      // Check length (typical Spark addresses are 90+ chars)
      if (address.length < 90) {
        return { valid: false, error: "Address too short" };
      }

      // Check for common typos
      if (address.includes(" ") || address.includes("\n")) {
        return { valid: false, error: "Address contains whitespace" };
      }

      return { valid: true };
    }
    ```
  </Accordion>

  <Accordion title="Handle Lightning Network specifics">
    Lightning Network has unique characteristics:

    ```javascript theme={null}
    const lightningLimits = {
      minAmount: 1000, // 0.00001 BTC (1000 satoshis)
      maxAmount: 10000000, // 0.1 BTC (10M satoshis)
      settlementTime: "Instant (typically < 10 seconds)",
      fees: "Very low (typically < 1%)",
    };

    function validateLightningAmount(satoshis) {
      if (satoshis < lightningLimits.minAmount) {
        throw new Error(`Minimum amount is ${lightningLimits.minAmount} sats`);
      }

      if (satoshis > lightningLimits.maxAmount) {
        throw new Error(`Maximum amount is ${lightningLimits.maxAmount} sats`);
      }

      return true;
    }
    ```
  </Accordion>

  <Accordion title="Provide clear user instructions">
    Help users understand the process:

    ```javascript theme={null}
    function getWalletInstructions(walletType) {
      return {
        SPARK_WALLET: {
          title: "Lightning Network Wallet",
          steps: [
            "Open your Lightning wallet app",
            'Select "Send" or "Pay"',
            "Scan the QR code or paste the address",
            "Confirm the amount and send",
            "Funds arrive instantly",
          ],
          compatibleWallets: [
            "Spark Wallet",
            "Phoenix",
            "Breez",
            "Muun",
            "Blue Wallet (Lightning)",
          ],
        },
      };
    }
    ```
  </Accordion>

  <Accordion title="Handle failed transfers">
    Implement retry logic for common failures:

    ```javascript theme={null}
    async function handleTransferFailure(transaction) {
      const { failureReason } = transaction;

      const retryableReasons = [
        "TEMPORARY_NETWORK_ERROR",
        "INSUFFICIENT_LIQUIDITY",
        "ROUTE_NOT_FOUND",
      ];

      if (retryableReasons.includes(failureReason)) {
        // Retry after delay
        await delay(5000);
        return await retryTransfer(transaction.quoteId);
      }

      // Non-retryable errors
      const errorMessages = {
        INVALID_ADDRESS:
          "The wallet address is invalid. Please verify and try again.",
        AMOUNT_TOO_SMALL:
          "Amount is below minimum. Lightning Network requires at least 1000 satoshis.",
        AMOUNT_TOO_LARGE:
          "Amount exceeds Lightning Network limits. Consider splitting into multiple transfers.",
      };

      return {
        canRetry: false,
        message:
          errorMessages[failureReason] ||
          "Transfer failed. Please contact support.",
      };
    }
    ```
  </Accordion>
</AccordionGroup>

## Testing in sandbox

Test self-custody wallet flows in sandbox mode:

```bash theme={null}
# Step 1: Create the external account for the Spark wallet
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/customers/external-accounts' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "customerId": "Customer:...",
    "currency": "BTC",
    "accountInfo": {
      "accountType": "SPARK_WALLET",
      "address": "spark1pgssyuuuhnrrdjswal5c3s3rafw9w3y5dd4cjy3duxlf7hjzkp0rqx6dj6mrhu"
    }
  }'

# Step 2: Create and execute the quote
curl -X POST 'https://api.lightspark.com/grid/2025-10-13/quotes' \
  -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
    "source": {
      "sourceType": "ACCOUNT",
      "accountId": "InternalAccount:..."
    },
    "destination": {
      "destinationType": "ACCOUNT",
      "accountId": "ExternalAccount:..."
    },
    "lockedCurrencySide": "SENDING",
    "lockedCurrencyAmount": 10000,
    "immediatelyExecute": true
  }'
```

<Tip>
  In sandbox mode, transfers to Spark wallets complete instantly without
  requiring actual Lightning Network transactions.
</Tip>

## Next steps

<FeatureCardGrid cols={2}>
  <FeatureCard icon="/images/icons/arrow-left-right.svg" title="Fiat-crypto conversion" href="/ramps/conversion-flows/fiat-crypto-conversion">
    Learn about on-ramp and off-ramp flows
  </FeatureCard>

  <FeatureCard icon="/images/icons/bank.svg" title="External accounts" href="/ramps/accounts/external-accounts">
    Manage external accounts including Spark wallets
  </FeatureCard>

  <FeatureCard icon="/images/icons/bell.svg" title="Webhooks" href="/ramps/platform-tools/webhooks">
    Set up webhooks to monitor transaction status
  </FeatureCard>

  <FeatureCard icon="/images/icons/sandbox.svg" title="Sandbox testing" href="/ramps/platform-tools/sandbox-testing">
    Test wallet integrations in sandbox mode
  </FeatureCard>
</FeatureCardGrid>
