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.

What you can build with this

  • 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

What you need, 30 seconds

  1. A Roxy API key. Get one on the pricing page.
  2. A text editor and a browser. That is it.

Step 1, call your first endpoint

Three separate tarot calls, same pattern. Pick a language.

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

Step 2, build the app

Create tarot.html and paste this block.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Tarot Reading</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 0 auto; padding: 20px; background: #0f0f1a; color: #e0e0e0; }
    h1 { text-align: center; margin-bottom: 24px; color: #c9a0dc; }
    .tabs { display: flex; gap: 8px; margin-bottom: 24px; justify-content: center; }
    .tabs button { padding: 10px 20px; border: 1px solid #333; border-radius: 8px; background: #1a1a2e; color: #aaa; cursor: pointer; }
    .tabs button.active { background: #7c3aed; color: white; border-color: #7c3aed; }
    .cards { display: flex; gap: 16px; justify-content: center; flex-wrap: wrap; margin-bottom: 24px; }
    .card { background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 16px; width: 200px; text-align: center; }
    .card img { width: 100%; border-radius: 8px; margin-bottom: 12px; }
    .card img.reversed { transform: rotate(180deg); }
    .card .position { font-size: 12px; text-transform: uppercase; color: #7c3aed; margin-bottom: 8px; }
    .card .name { font-size: 16px; font-weight: 600; margin-bottom: 4px; }
    .card .rev { font-size: 11px; color: #e57373; margin-bottom: 4px; }
    .card .kw { font-size: 12px; color: #999; margin-bottom: 8px; }
    .card .meaning { font-size: 13px; line-height: 1.5; color: #ccc; }
    .answer { text-align: center; background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 24px; margin-bottom: 24px; font-size: 48px; font-weight: 700; color: #c9a0dc; }
    .message { text-align: center; font-size: 15px; line-height: 1.6; color: #ccc; margin-bottom: 24px; }
    .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; }
  </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>
  <div id="out"></div>

  <script>
    const API_KEY = 'YOUR_API_KEY';
    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');
        document.getElementById('out').innerHTML = '';
      };
    });

    function card(c, position) {
      return '<div class="card">' +
        (position ? '<div class="position">' + position + '</div>' : '') +
        (c.imageUrl ? '<img src="' + c.imageUrl + '" alt="' + c.name + '"' + (c.reversed ? ' class="reversed"' : '') + ' />' : '') +
        '<div class="name">' + c.name + '</div>' +
        (c.reversed ? '<div class="rev">Reversed</div>' : '') +
        '<div class="kw">' + (c.keywords || []).join(' &middot; ') + '</div>' +
        '<div class="meaning">' + (c.meaning || '') + '</div>' +
        '</div>';
    }

    document.getElementById('draw').onclick = async () => {
      const out = document.getElementById('out');
      out.innerHTML = '<div style="text-align:center;padding:40px;color:#888;">Shuffling...</div>';

      const urls = {
        'daily': 'https://roxyapi.com/api/v2/tarot/daily',
        'three-card': 'https://roxyapi.com/api/v2/tarot/spreads/three-card',
        'yes-no': 'https://roxyapi.com/api/v2/tarot/yes-no',
      };

      const res = await fetch(urls[mode], {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-API-Key': API_KEY },
        body: JSON.stringify({}),
      });
      const data = await res.json();

      if (mode === 'daily') {
        out.innerHTML =
          '<div class="cards">' + card(data.card) + '</div>' +
          '<div class="message">' + data.dailyMessage + '</div>';
      } else if (mode === 'three-card') {
        out.innerHTML =
          '<div class="cards">' + data.positions.map(p => card(p.card, p.label || p.name)).join('') + '</div>' +
          (data.reading ? '<div class="message">' + data.reading + '</div>' : '');
      } else {
        out.innerHTML =
          '<div class="answer">' + data.answer + '</div>' +
          '<div class="cards">' + card(data.card) + '</div>' +
          (data.interpretation ? '<div class="message">' + data.interpretation + '</div>' : '');
      }
    };
  </script>
</body>
</html>

Replace YOUR_API_KEY. Save. Double-click.

Step 3, add Celtic Cross

One more endpoint, same pattern. POST /tarot/spreads/celtic-cross returns 10 positions. Add a fourth tab with mode: 'celtic-cross', add the URL to the map, the render logic already handles positions[].

Step 4, ship it

Three 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 keys are for prototyping only.
  3. Cache the card catalog. Call GET /tarot/cards once, store it. Users can browse the full 78-card deck without hitting the API per card view.

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

Gotchas

  • Daily card returns dailyMessage, not interpretation. That field is on yes-no. Three-card uses reading at the top level plus per-position interpretation.
  • Reversed cards set card.reversed: true. Flip the image with CSS (transform: rotate(180deg)) and show a "Reversed" badge. 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. Reset happens at UTC midnight.
  • imageUrl points to our CDN. No image hosting needed. The path is stable enough to cache client-side.
  • Three-card positions[] is always length 3, with label values Past, Present, Future. Celtic Cross returns length 10 with numbered positions.

What to build next

  • The AI chatbot tutorial wires tarot tools to an LLM so users can ask natural-language questions.
  • The tarot guide lists every spread and field shape.
  • The dating app tutorial shows the astrology surface your users may want after a tarot reading.