1. Docs
  2. What To Build
  3. Tarot Reading App

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.
  2. A text editor and a browser. No build step.
  3. No Location API needed. Tarot endpoints are stateless and seedable.

Install

npm install @roxyapi/sdk

Call the endpoints

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

# 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"}'

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).

<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. No hand-built card markup, no reversed-flip CSS to maintain: the components own all of it.

<!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 (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 is the full mobile build: React Native + Expo + TypeScript, full 78-card Rider-Waite deck, Celtic Cross spreads, daily readings.

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.

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 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