---
title: Transaction Requests
description:
  Build interactive payment flows with server-side transaction creation
---

Transaction requests enable interactive payment flows by allowing wallets to
communicate with your server to compose any Solana transaction. This unlocks
advanced use cases like NFT minting, dynamic pricing, multi-step DeFi
transactions, and custom business logic.

## Overview

Transaction requests follow this URL format:

```
solana:<server-endpoint-url>
```

The wallet makes two HTTP requests to your endpoint:

1. **GET request** - Retrieve payment information (label, icon)
2. **POST request** - Get the transaction to sign

## Basic Setup

### 1. Install Dependencies

```bash
pnpm add @solana/pay@beta @solana/kit @solana-program/system \
  @solana/kit-plugin-rpc @solana/kit-plugin-payer @solana/kit-plugin-instruction-plan
```

### 2. Create API Endpoint

Set up an API endpoint that handles both GET and POST requests:

```typescript
// pages/api/pay.ts (Next.js) or similar endpoint
import { address, createNoopSigner, lamports } from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
import { createMerchantClient } from "@solana/pay";

const MERCHANT_WALLET = address(process.env.MERCHANT_WALLET);

export default async function handler(req, res) {
  if (req.method === "GET") {
    return handleGet(req, res);
  }

  if (req.method === "POST") {
    return handlePost(req, res);
  }

  return res.status(405).json({ error: "Method not allowed" });
}

// GET request handler
async function handleGet(req, res) {
  const label = "My Store";
  const icon = "https://mystore.com/icon.png";

  res.status(200).json({
    label,
    icon
  });
}

// POST request handler
async function handlePost(req, res) {
  try {
    const { account } = req.body;

    if (!account) {
      return res.status(400).json({ error: "Missing account parameter" });
    }

    const buyerAddress = address(account);

    // Create the merchant client for building the transaction
    const merchant = createMerchantClient({
      rpcUrl: process.env.SOLANA_RPC_URL
    });

    // Build transfer instruction (createNoopSigner because the wallet signs later)
    const instruction = getTransferSolInstruction({
      source: createNoopSigner(buyerAddress),
      destination: MERCHANT_WALLET,
      amount: lamports(100_000_000n) // 0.1 SOL
    });

    // Build a base64-encoded transaction
    const transaction = merchant.pay.buildTransaction(buyerAddress, [
      instruction
    ]);

    res.status(200).json({
      transaction,
      message: "Thanks for your purchase!"
    });
  } catch (error) {
    console.error("Transaction creation error:", error);
    res.status(500).json({ error: "Failed to create transaction" });
  }
}
```

### 3. Create Payment URL

Generate the transaction request URL:

```typescript
import { encodeURL } from "@solana/pay";

// Your API endpoint
const apiUrl = "https://yoursite.com/api/pay";

// Create transaction request URL
const url = encodeURL({
  link: new URL(apiUrl)
});

console.log(url.toString());
// Output: solana:https://yoursite.com/api/pay
```

## Wallet-Side: Fetching Transaction Requests

On the wallet side, use `fetchTransaction` to retrieve and sign a transaction
from a transaction request URL:

```typescript
import { createWalletClient } from "@solana/pay";

const wallet = createWalletClient({
  rpcUrl: "https://api.mainnet-beta.solana.com",
  payer: myWalletSigner
});

// Parse the Solana Pay URL
const parsed = wallet.pay.parseURL(url);

if ("link" in parsed) {
  // This is a transaction request — fetch the transaction from the server
  const transaction = await wallet.pay.fetchTransaction(
    myWalletSigner.address,
    parsed.link
  );
  // Sign and send the transaction...
}
```

## Advanced Examples

### SPL Token Payment

Create a transaction for USDC payment:

```typescript
import { address } from "@solana/kit";
import {
  fetchMint,
  findAssociatedTokenPda,
  getTransferCheckedInstruction,
  TOKEN_PROGRAM_ADDRESS
} from "@solana-program/token";

async function createTokenPaymentInstruction(rpc, buyer, amount) {
  const USDC_MINT = address("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");

  // Get mint info for decimals
  const mint = await fetchMint(rpc, USDC_MINT);

  // Derive associated token accounts
  const [buyerATA] = await findAssociatedTokenPda({
    owner: buyer,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
    mint: USDC_MINT
  });
  const [merchantATA] = await findAssociatedTokenPda({
    owner: MERCHANT_WALLET,
    tokenProgram: TOKEN_PROGRAM_ADDRESS,
    mint: USDC_MINT
  });

  // Calculate token amount with decimals
  const tokenAmount = BigInt(Math.round(amount * 10 ** mint.data.decimals));

  return getTransferCheckedInstruction({
    source: buyerATA,
    mint: USDC_MINT,
    destination: merchantATA,
    authority: buyer,
    amount: tokenAmount,
    decimals: mint.data.decimals
  });
}
```

### Dynamic Pricing with Discounts

Implement dynamic pricing based on customer data:

```typescript
async function handlePost(req, res) {
  const { account } = req.body;
  const buyer = address(account);

  // Check if customer holds discount NFT
  const hasDiscountNFT = await checkForDiscountNFT(buyer);

  // Calculate dynamic price
  let price = 10; // $10 base price
  if (hasDiscountNFT) {
    price = price * 0.8; // 20% discount
  }

  const merchant = createMerchantClient({
    rpcUrl: process.env.SOLANA_RPC_URL
  });
  const instruction = await createTokenPaymentInstruction(
    merchant.rpc,
    buyer,
    price
  );
  const transaction = merchant.pay.buildTransaction(buyer, [instruction]);

  const message = hasDiscountNFT
    ? `20% discount applied! Total: $${price.toFixed(2)}`
    : `Total: $${price.toFixed(2)}`;

  res.status(200).json({ transaction, message });
}
```

### Order Management Integration

Integrate with your existing order system:

```typescript
import { address, AccountRole, createNoopSigner, lamports } from "@solana/kit";

// Enhanced POST handler with order management
async function handlePost(req, res) {
  const { account } = req.body;
  const { orderId } = req.query;

  if (!orderId) {
    return res.status(400).json({ error: "Missing order ID" });
  }

  // Get order details
  const order = await db.orders.findById(orderId);
  if (!order || order.status !== "pending") {
    return res.status(400).json({ error: "Invalid order" });
  }

  const buyer = address(account);

  // Create payment instruction (createNoopSigner because the wallet signs later)
  const instruction = getTransferSolInstruction({
    source: createNoopSigner(buyer),
    destination: MERCHANT_WALLET,
    amount: lamports(BigInt(Math.round(order.total * 1e9)))
  });

  // Add reference for tracking (accounts is readonly, so spread into a new instruction)
  const withRef = {
    ...instruction,
    accounts: [
      ...(instruction.accounts ?? []),
      {
        address: address(order.referenceKey),
        role: AccountRole.READONLY
      }
    ]
  };

  const merchant = createMerchantClient({ rpcUrl: process.env.SOLANA_RPC_URL });
  const transaction = merchant.pay.buildTransaction(buyer, [withRef]);

  // Update order status
  await db.orders.update(orderId, {
    status: "processing",
    walletAddress: account
  });

  res.status(200).json({
    transaction,
    message: `Order ${order.id} - ${order.items.length} items`
  });
}
```

## Payment Validation

Validate completed transactions:

```typescript
import { address } from "@solana/kit";
import { createMerchantClient } from "@solana/pay";

const merchant = createMerchantClient({
  rpcUrl: "https://api.mainnet-beta.solana.com"
});

async function validatePayment(reference, recipient, amount) {
  try {
    const found = await merchant.pay.findReference(reference);

    // findReference only locates a signature mentioning the reference address.
    // validateTransfer checks recipient, amount, and token match.
    await merchant.pay.validateTransfer(found.signature, {
      recipient,
      amount,
      reference
    });

    return {
      success: true,
      signature: found.signature
    };
  } catch (error) {
    console.error("Payment validation error:", error);
  }

  return { success: false };
}
```

## Error Handling

Implement robust error handling:

```typescript
async function handlePost(req, res) {
  try {
    const { account } = req.body;

    if (!account) {
      return res.status(400).json({
        error: "Missing account parameter",
        code: "MISSING_ACCOUNT"
      });
    }

    let buyer;
    try {
      buyer = address(account);
    } catch {
      return res.status(400).json({
        error: "Invalid account address",
        code: "INVALID_ACCOUNT"
      });
    }

    const instruction = getTransferSolInstruction({
      source: createNoopSigner(buyer),
      destination: MERCHANT_WALLET,
      amount: lamports(100_000_000n)
    });

    const merchant = createMerchantClient({
      rpcUrl: process.env.SOLANA_RPC_URL
    });
    const transaction = merchant.pay.buildTransaction(buyer, [instruction]);

    res.status(200).json({
      transaction,
      message: "Payment ready"
    });
  } catch (error) {
    console.error("Transaction request error:", error);

    res.status(500).json({
      error: "Internal server error",
      code: "INTERNAL_ERROR"
    });
  }
}
```

## Best Practices

### Security

- Validate all input parameters
- Calculate amounts server-side only
- Use HTTPS for all endpoints
- Implement rate limiting
- Store sensitive data securely

### Performance

- Cache frequently accessed data
- Use connection pooling
- Implement request timeouts
- Consider using webhooks for payment confirmation

### User Experience

- Provide clear error messages
- Show transaction progress
- Handle network issues gracefully
- Support transaction retries
