# Build a tarot reading app

> Ship a three-mode tarot app (daily card, three-card spread, yes-no oracle) with card imagery and reversed handling. Time to ship: 30 minutes.

Tarot is the highest-frequency divination surface Roxy ships. This tutorial wires the three most-popular features: daily card for daily active users, three-card for the entry reading, yes-no for impulse queries. One HTML file, card imagery included. Then upgrade to `<roxy-tarot-card>` / `<roxy-tarot-spread>` for a drop-in renderer.

## What you can build

- Daily tarot widgets for wellness and lifestyle apps
- Three-card past/present/future spreads
- Yes-no oracles for impulse decisions
- Celtic Cross readers (10-position premium spread)
- Love tarot spreads for dating apps
- Cached card browsers across the full 78-card deck

## Prerequisites

1. A Roxy API key from [/account](/account).
2. A text editor and a browser. No build step.
3. No Location API needed. Tarot endpoints are stateless and seedable.

## Install


### npm
```bash
npm install @roxyapi/sdk
```

### Python
```bash
pip install roxy-sdk
```

### PHP
```bash
composer require roxyapi/sdk
```

### CDN (for `<roxy-tarot-*>` components)
```html
<script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@0/dist/cdn/roxy-ui.js" defer></script>
```

## Call the endpoints

Three independent calls, same shape. Verified operationIds: `getDailyCard`, `castThreeCard`, `castYesNo`, `castCelticCross`.


### curl
```bash
# Daily card
curl -X POST https://roxyapi.com/api/v2/tarot/daily \
  -H "X-API-Key: $ROXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"seed":"user-42"}'

# Three-card spread
curl -X POST https://roxyapi.com/api/v2/tarot/spreads/three-card \
  -H "X-API-Key: $ROXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question":"what should I focus on this week"}'

# Yes or no
curl -X POST https://roxyapi.com/api/v2/tarot/yes-no \
  -H "X-API-Key: $ROXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question":"should I take the job"}'
```

### TypeScript SDK
```typescript
import { createRoxy } from '@roxyapi/sdk';

const roxy = createRoxy(process.env.ROXY_API_KEY!);

const { data: daily } = await roxy.tarot.getDailyCard({ body: { seed: 'user-42' } });
console.log(daily.card.name, daily.card.imageUrl);

const { data: spread } = await roxy.tarot.castThreeCard({
  body: { question: 'what should I focus on this week' },
});
spread.positions.forEach(p => console.log(p.name, p.card.name));

const { data: yn } = await roxy.tarot.castYesNo({
  body: { question: 'should I take the job' },
});
console.log(yn.answer); // "Yes" | "No" | "Maybe"
```

### Python SDK
```python
import os
from roxy_sdk import create_roxy

roxy = create_roxy(os.environ['ROXY_API_KEY'])

daily = roxy.tarot.get_daily_card(seed='user-42')
print(daily['card']['name'])

spread = roxy.tarot.cast_three_card(question='what should I focus on this week')
for p in spread['positions']:
    print(p['name'], p['card']['name'])

yn = roxy.tarot.cast_yes_no(question='should I take the job')
print(yn['answer'])
```

### PHP SDK
```php
<?php

use function RoxyAPI\Sdk\createRoxy;

$roxy = createRoxy(getenv('ROXY_API_KEY'));

$daily = $roxy->tarot->getDailyCard(seed: 'user-42');
echo $daily['card']['name'];

$spread = $roxy->tarot->castThreeCard(question: 'what should I focus on this week');
foreach ($spread['positions'] as $p) {
    echo $p['name'], ' ', $p['card']['name'], "\n";
}

$yn = $roxy->tarot->castYesNo(question: 'should I take the job');
echo $yn['answer'];
```

### MCP
```bash
claude mcp add-json --scope user roxy-tarot '{"type":"http","url":"https://roxyapi.com/mcp/tarot","headers":{"X-API-Key":"YOUR_KEY"}}'
```

Then ask any MCP client: "draw a daily tarot card for user 42" or "cast a three-card spread for me about my career."

## Render the result

### Option A: drop-in web components

`<roxy-tarot-card>` renders a single card. `<roxy-tarot-spread>` renders any spread (`three-card`, `celtic-cross`, `love`, `yes-no`, `draw`).

```html
<script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@0/dist/cdn/roxy-ui.js" defer></script>

<roxy-tarot-card id="daily-card"></roxy-tarot-card>
<roxy-tarot-spread id="spread" spread="three-card"></roxy-tarot-spread>

<script type="module">
  import { createRoxy } from 'https://cdn.jsdelivr.net/npm/@roxyapi/sdk@latest/dist/factory.js';
  const roxy = createRoxy('YOUR_PUBLISHABLE_KEY');

  const { data: daily } = await roxy.tarot.getDailyCard({ body: { seed: 'user-42' } });
  document.getElementById('daily-card').data = daily;

  const { data: spread } = await roxy.tarot.castThreeCard({
    body: { question: 'what should I focus on this week' },
  });
  document.getElementById('spread').data = spread;
</script>
```

Both components handle reversed cards (image flip), keywords, meanings, and per-position labels for spreads. Theme via CSS custom properties.

### Option B: three-mode app, components do the render

Same single file with tabs and one Draw button, but the components handle every card. The vanilla JS only switches mode, calls the endpoint, and assigns the unwrapped response to `<roxy-tarot-card>` (daily) or `<roxy-tarot-spread>` (three-card, yes-no) via Pattern 1 from the [UI components page](/docs/ui). No hand-built card markup, no reversed-flip CSS to maintain: the components own all of it.

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Tarot Reading</title>
  <script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@0/dist/cdn/roxy-ui.js" defer></script>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 0 auto; padding: 20px; }
    h1 { text-align: center; margin-bottom: 24px; }
    .tabs { display: flex; gap: 8px; margin-bottom: 24px; justify-content: center; }
    .tabs button { padding: 10px 20px; border: 1px solid #ccc; border-radius: 8px; background: #fff; cursor: pointer; }
    .tabs button.active { background: #7c3aed; color: white; border-color: #7c3aed; }
    .draw-btn { display: block; width: 100%; max-width: 300px; margin: 0 auto 24px; padding: 14px; background: #7c3aed; color: white; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; }
    [hidden] { display: none; }
  </style>
</head>
<body>
  <h1>Tarot Reading</h1>

  <div class="tabs">
    <button class="active" data-mode="daily">Daily Card</button>
    <button data-mode="three-card">Past / Present / Future</button>
    <button data-mode="yes-no">Yes / No</button>
  </div>

  <button class="draw-btn" id="draw">Draw</button>

  <roxy-tarot-card id="daily-card" hidden></roxy-tarot-card>
  <roxy-tarot-spread id="spread" hidden></roxy-tarot-spread>

  <script type="module">
    import { createRoxy } from 'https://cdn.jsdelivr.net/npm/@roxyapi/sdk@latest/dist/factory.js';

    // Browser-side: use a publishable key once available, or proxy through a backend.
    const roxy = createRoxy('YOUR_PUBLISHABLE_KEY');

    const dailyCard = document.getElementById('daily-card');
    const spread = document.getElementById('spread');
    let mode = 'daily';

    document.querySelectorAll('.tabs button').forEach((b) => {
      b.onclick = () => {
        mode = b.dataset.mode;
        document.querySelectorAll('.tabs button').forEach((x) => x.classList.remove('active'));
        b.classList.add('active');
        dailyCard.hidden = true;
        spread.hidden = true;
      };
    });

    document.getElementById('draw').onclick = async () => {
      if (mode === 'daily') {
        const { data } = await roxy.tarot.getDailyCard({ body: {} });
        dailyCard.data = data;
        dailyCard.hidden = false;
        spread.hidden = true;
      } else if (mode === 'three-card') {
        const { data } = await roxy.tarot.castThreeCard({ body: {} });
        spread.spread = 'three-card';
        spread.data = data;
        spread.hidden = false;
        dailyCard.hidden = true;
      } else {
        const { data } = await roxy.tarot.castYesNo({ body: {} });
        spread.spread = 'yes-no';
        spread.data = data;
        spread.hidden = false;
        dailyCard.hidden = true;
      }
    };
  </script>
</body>
</html>
```

`<roxy-tarot-card>` renders the daily card with reversed flip, keywords, and message. `<roxy-tarot-spread>` renders the three-card positions and the yes-no answer with its interpretation: set `spread` to match the response (`three-card`, `yes-no`, `celtic-cross`, `love`, `draw`). Theme both with CSS custom properties.

## Add Celtic Cross

One more endpoint, same shape: [`POST /tarot/spreads/celtic-cross`](/api-reference#tag/tarot/POST/tarot/spreads/celtic-cross) (`castCelticCross`). Returns 10 positions. Add a fourth tab with `data-mode="celtic-cross"`, call `roxy.tarot.castCelticCross({ body: {} })`, set `spread.spread = 'celtic-cross'`, then assign `spread.data`. `<roxy-tarot-spread>` renders all 10 positions with no extra code.

## Ready-made starter

[`tarot-starter-app`](https://github.com/RoxyAPI/tarot-starter-app) is the full mobile build: React Native + Expo + TypeScript, full 78-card Rider-Waite deck, Celtic Cross spreads, daily readings.

```bash
git clone https://github.com/RoxyAPI/tarot-starter-app
cd tarot-starter-app
npm install
echo "EXPO_PUBLIC_ROXYAPI_KEY=your_key" > .env.local
npm start
```

Browse the live catalog at [/starters/tarot-starter-app](/starters).

## Production tweaks

1. **Seed the daily card per user.** Pass `{"seed": userId}` in the body. Same user gets the same card all day. Perfect for push notifications and daily email cadence.
2. **Move the key server-side.** Browser-embedded secret keys are for prototyping only. Use a publishable key (`pk_live_*`, origin-restricted, coming soon) or proxy.
3. **Cache the card catalog.** Call [`GET /tarot/cards`](/api-reference#tag/tarot/GET/tarot/cards) once, store it. Users browse the full 78-card deck without hitting the API per view.

Deploy to Cloudflare Pages or Vercel. Free tier covers thousands of visitors a month.

## Gotchas

- **Each mode names its reading field differently.** Daily card returns `dailyMessage`, yes-no returns `answer` plus `interpretation`, three-card returns `summary` at the top plus per-position `interpretation`. The components read the right field per shape, so you only pass the unwrapped response.
- **Reversed cards set `card.reversed: true`.** The components flip the image and label it automatically. The `meaning` text is already context-aware.
- **Seeded is deterministic per day.** Pass `seed: userId` and the same user gets the same daily card for the whole calendar date. Resets at UTC midnight.
- **`imageUrl` points to the Roxy CDN.** No image hosting needed. The path is stable enough to cache client-side.
- **Three-card `positions[]` is always length 3,** with `name` values `Past`, `Present`, `Future`. Celtic Cross returns length 10.

## What to build next

- The [AI chatbot tutorial](/docs/tutorials/ai-chatbot) wires tarot tools to an LLM so users ask natural-language questions.
- The [tarot guide](/docs/guides/tarot) lists every spread and field.
- The [dating app tutorial](/docs/tutorials/dating-app) shows the astrology surface for cross-sell after a tarot reading.
