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

# Integrate Tip Stack in a React Application

> Use Tip Stack's SDK API to initialize a session, trigger tips, and listen for real-time tip events inside your React or Next.js application.

The Tip Stack SDK API gives React and Next.js applications full programmatic control over the tipping flow. Instead of dropping in a static HTML snippet, you call three lightweight REST endpoints — initialize a session, trigger a tip intent, and subscribe to confirmed-tip events — and build your own UI on top.

<Warning>
  Never expose your API key on the client side. Make the `/sdk/init` call from your backend and return only the `sessionToken` to your frontend.
</Warning>

## Integration steps

<Steps>
  <Step title="Get your API key">
    Open your [creator dashboard](https://tipstack.fun/dashboard) and navigate to **Settings → Developer → API Keys**. Generate a new secret key and store it securely in your server-side environment variables (e.g. `.env.local` for Next.js).

    ```bash .env.local theme={null}
    TIPSTACK_API_KEY=your_secret_api_key_here
    ```
  </Step>

  <Step title="Initialize an SDK session">
    Call `POST /api/sdk/init` from your **server** (an API route, a Server Component, or a Next.js Route Handler). Pass your API key in the `Authorization` header along with the creator's ID and the origin URL of the embedding page.

    ```ts app/api/tipstack/session/route.ts theme={null}
    // Next.js App Router — server-side only
    export async function POST(request: Request) {
      const { creatorId } = await request.json();

      const res = await fetch('https://tipstack.fun/api/sdk/init', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.TIPSTACK_API_KEY}`,
        },
        body: JSON.stringify({
          creatorId,
          originUrl: process.env.NEXT_PUBLIC_SITE_URL, // e.g. https://myblog.com
          theme: 'dark',
        }),
      });

      const data = await res.json();
      // Return only the sessionToken — never forward your API key
      return Response.json({ sessionToken: data.sessionToken, embedUrl: data.config.embedUrl });
    }
    ```

    A successful response looks like:

    ```json theme={null}
    {
      "success": true,
      "sessionToken": "sdk_sess_<uuid>",
      "config": {
        "creatorId": "<uuid>",
        "creatorAddress": "Ab3C...7xYz",
        "acceptedTokens": ["SOL", "USDC"],
        "embedUrl": "https://tipstack.fun/checkout/<uuid>?theme=dark"
      }
    }
    ```

    The `sessionToken` (prefixed `sdk_sess_`) authorizes all subsequent SDK calls for this page load. The `embedUrl` is a fully hosted checkout page you can render directly in an `<iframe>` — see [Rendering the hosted checkout](#rendering-the-hosted-checkout) below.
  </Step>

  <Step title="Trigger a tip">
    Call `POST /api/sdk/tip` from the client using the `sessionToken` returned in the previous step. Supply the tip amount, the token mint address, the sender's wallet address, and the creator's ID.

    ```ts theme={null}
    const sendTip = async (sessionToken: string, creatorId: string, amount: number) => {
      const res = await fetch('https://tipstack.fun/api/sdk/tip', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${sessionToken}`,
        },
        body: JSON.stringify({
          creatorId,
          amount,                  // USD value, e.g. 5
          inputTokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC mint
          sourceWalletAddress: 'YOUR_WALLET_ADDRESS',
        }),
      });

      const data = await res.json();
      // { success: true, intentId: "pi_<uuid>", status: "requires_action" }
      return data.intentId;
    };
    ```

    The response returns an `intentId` and a `status` of `requires_action`, indicating the user must sign the transaction in their wallet to complete the tip.
  </Step>

  <Step title="Listen for confirmed tip events">
    Poll `GET /api/sdk/events` with your **API key** (server-side) to receive a list of confirmed tip events for a creator. Use the optional `since` query parameter to fetch only events after a given ISO timestamp.

    ```ts app/api/tipstack/events/route.ts theme={null}
    export async function GET(request: Request) {
      const { searchParams } = new URL(request.url);
      const creatorId = searchParams.get('creatorId');
      const since = searchParams.get('since') ?? '';

      const url = new URL('https://tipstack.fun/api/sdk/events');
      url.searchParams.set('creatorId', creatorId ?? '');
      if (since) url.searchParams.set('since', since);

      const res = await fetch(url.toString(), {
        headers: {
          'Authorization': `Bearer ${process.env.TIPSTACK_API_KEY}`,
        },
      });

      return Response.json(await res.json());
    }
    ```

    Each event in the response array has this shape:

    ```json theme={null}
    {
      "eventId": "<txSignature>",
      "type": "tip_completed",
      "amountUsdc": 5,
      "amountRaw": 5,
      "tokenSymbol": "USDC",
      "sender": "<walletAddress>",
      "timestamp": "2024-11-01T12:34:56.000Z",
      "txSignature": "<txSignature>"
    }
    ```

    Up to **50 confirmed tips** are returned per request, ordered newest-first.
  </Step>
</Steps>

## Full React example

The component below combines all three steps into a self-contained `TipButton`. The `/sdk/init` call is proxied through your own Next.js API route so the API key never reaches the browser.

```tsx TipButton.tsx theme={null}
import { useState, useEffect } from 'react';

export function TipButton({ creatorId }: { creatorId: string }) {
  const [sessionToken, setSessionToken] = useState<string | null>(null);

  useEffect(() => {
    // Call your own server-side proxy — never call /sdk/init directly from the client
    fetch('/api/tipstack/session', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ creatorId }),
    })
      .then(r => r.json())
      .then(data => setSessionToken(data.sessionToken));
  }, [creatorId]);

  const sendTip = async (amount: number) => {
    if (!sessionToken) return;
    const res = await fetch('https://tipstack.fun/api/sdk/tip', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${sessionToken}`,
      },
      body: JSON.stringify({
        creatorId,
        amount,
        inputTokenMint: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
        sourceWalletAddress: 'YOUR_WALLET_ADDRESS',
      }),
    });
    const data = await res.json();
    console.log('Tip intent:', data.intentId);
  };

  return (
    <button onClick={() => sendTip(5)} disabled={!sessionToken}>
      ⚡ Tip $5
    </button>
  );
}
```

<Warning>
  The `/sdk/init` call in the example above goes through `/api/tipstack/session` — your own server-side route that holds the API key. Never call `https://tipstack.fun/api/sdk/init` directly from the browser; doing so would expose your secret API key to anyone who inspects network requests.
</Warning>

## Rendering the hosted checkout

If you prefer a fully managed UI instead of building your own, render the `embedUrl` from `/sdk/init` inside an `<iframe>`. Tip Stack hosts and updates the checkout experience — you just provide the container.

```tsx EmbedCheckout.tsx theme={null}
export function EmbedCheckout({ embedUrl }: { embedUrl: string }) {
  return (
    <iframe
      src={embedUrl}
      title="Tip Stack Checkout"
      width="100%"
      height="480"
      allow="clipboard-write"
      style={{ border: 'none', borderRadius: '16px' }}
    />
  );
}
```

<Tip>
  The `embedUrl` includes your chosen theme as a query parameter (e.g. `?theme=dark`). To switch themes, re-initialize the session with a different `theme` value.
</Tip>

## Listening for tip events in the client

To update your UI in real time when a tip is confirmed, poll your own events proxy on a short interval and compare the latest `timestamp`.

```tsx useTipEvents.ts theme={null}
import { useState, useEffect, useRef } from 'react';

interface TipEvent {
  eventId: string;
  type: string;
  amountUsdc: number;
  amountRaw: number;
  tokenSymbol: string;
  sender: string;
  timestamp: string;
  txSignature: string;
}

export function useTipEvents(creatorId: string, pollIntervalMs = 5000) {
  const [events, setEvents] = useState<TipEvent[]>([]);
  const sinceRef = useRef<string>(new Date().toISOString());

  useEffect(() => {
    const poll = async () => {
      const res = await fetch(
        `/api/tipstack/events?creatorId=${creatorId}&since=${sinceRef.current}`
      );
      const data = await res.json();

      if (data.events?.length) {
        setEvents(prev => [...data.events, ...prev]);
        // Advance the cursor to avoid re-fetching the same events
        sinceRef.current = data.events[0].timestamp;
      }
    };

    const interval = setInterval(poll, pollIntervalMs);
    return () => clearInterval(interval);
  }, [creatorId, pollIntervalMs]);

  return events;
}
```

## API reference

<AccordionGroup>
  <Accordion title="POST /api/sdk/init">
    Initializes a new SDK session and validates the embedding origin.

    **Headers**

    | Header          | Value                 |
    | --------------- | --------------------- |
    | `Authorization` | `Bearer YOUR_API_KEY` |
    | `Content-Type`  | `application/json`    |

    **Body**

    | Field       | Type     | Required | Description                                                   |
    | ----------- | -------- | -------- | ------------------------------------------------------------- |
    | `creatorId` | `string` | Yes      | Creator handle, wallet address, or user ID.                   |
    | `originUrl` | `string` | Yes      | Full origin of the embedding site, e.g. `https://myblog.com`. |
    | `theme`     | `string` | No       | `dark` (default) or `light`.                                  |

    **Response** — `200 OK`

    | Field                   | Description                                                 |
    | ----------------------- | ----------------------------------------------------------- |
    | `sessionToken`          | Short-lived token (`sdk_sess_<uuid>`) for subsequent calls. |
    | `config.creatorId`      | The creator's UUID.                                         |
    | `config.creatorAddress` | The creator's wallet address, masked (e.g. `Ab3C...7xYz`).  |
    | `config.acceptedTokens` | Array of accepted token symbols: `["SOL", "USDC"]`.         |
    | `config.embedUrl`       | Hosted checkout URL ready to use in an `<iframe>`.          |
  </Accordion>

  <Accordion title="POST /api/sdk/tip">
    Creates a tip intent within the current SDK session.

    **Headers**

    | Header          | Value                                                     |
    | --------------- | --------------------------------------------------------- |
    | `Authorization` | `Bearer sdk_sess_<uuid>` (session token from `/sdk/init`) |
    | `Content-Type`  | `application/json`                                        |

    **Body**

    | Field                 | Type     | Required | Description                                                                          |
    | --------------------- | -------- | -------- | ------------------------------------------------------------------------------------ |
    | `creatorId`           | `string` | Yes      | Creator to tip.                                                                      |
    | `amount`              | `number` | Yes      | USD amount of the tip.                                                               |
    | `inputTokenMint`      | `string` | Yes      | SPL token mint address. Use `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` for USDC. |
    | `sourceWalletAddress` | `string` | Yes      | Sender's Solana wallet address.                                                      |

    **Response** — `200 OK`

    | Field      | Description                                                    |
    | ---------- | -------------------------------------------------------------- |
    | `intentId` | Unique tip intent ID (`pi_<uuid>`).                            |
    | `status`   | Always `requires_action` — the user must sign in their wallet. |
  </Accordion>

  <Accordion title="GET /api/sdk/events">
    Returns confirmed tip events for a creator, newest-first.

    **Headers**

    | Header          | Value                 |
    | --------------- | --------------------- |
    | `Authorization` | `Bearer YOUR_API_KEY` |

    **Query parameters**

    | Parameter   | Required | Description                                                   |
    | ----------- | -------- | ------------------------------------------------------------- |
    | `creatorId` | Yes      | The creator's ID to fetch events for.                         |
    | `since`     | No       | ISO 8601 timestamp. Only events after this time are returned. |

    **Response** — `200 OK`

    Returns up to 50 `tip_completed` events. Each event includes `eventId`, `type`, `amountUsdc`, `amountRaw`, `tokenSymbol`, `sender`, `timestamp`, and `txSignature`.
  </Accordion>
</AccordionGroup>
