Skip to content
  1. Docs
  2. Integrations
  3. Shopify

Shopify astrology app, tarot and horoscope store

Add a tarot reading section, a daily horoscope on the customer account page, a post-checkout numerology email, or a kundli generator product on any Shopify store. Paste a widget in five minutes, or run an App Proxy when the reading has to be customer-scoped. The API key never touches the storefront.

Two rungs. A copy-paste widget draws its own form and renders the reading with no app and no server. The App Proxy is the step up: a secret key on your backend, readings seeded by customer.id, checkout and post-purchase flows.

Rung 1: paste a widget into a Custom Liquid section

For a page that only needs to display a reading (a tarot card, a daily horoscope, a natal chart), skip the app server entirely.

  1. Mint a publishable key at Account, API Keys: click New key, choose Publishable and Live, and add your store host to Allowed origins. Add both yourstore.myshopify.com and your custom domain if you use one. A pk_ key is built for the browser and cannot read your account.
  2. Open the widgets gallery, paste the key into the box at the top, pick a widget, and copy the snippet.
  3. In your Shopify admin go to Online Store, Themes, Customize. In the sidebar click Add section, use the Search sections field to find Custom Liquid, paste the snippet into it, and click Save.
<script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@latest/dist/cdn/widgets.js" defer></script>
<div data-roxy-widget="tarot-spread" data-publishable-key="pk_live_YOUR_KEY"></div>

Swap tarot-spread for any slug on the gallery: natal-chart, horoscope-card, vedic-kundli, numerology-card, moon-phase and more. A Custom Liquid block in the Header or Footer area shows on every page of the store. Full delivery-mode reference and error fixes: the widgets guide.

Reach for the App Proxy below when you need a secret key on the server: customer-scoped surfaces (a card seeded by customer.id), checkout flows, or post-purchase webhooks.

What you can build on Shopify

  • Tarot card-of-the-day section on the homepage, seeded by customer.id so each shopper sees a stable card
  • Daily horoscope on the customer account page bound to a saved birth date
  • Natal chart product page that generates the chart at checkout completion
  • Vedic kundli generator with a PDF emailed after the orders/paid webhook
  • Numerology Life Path calculator as a pre-purchase product configurator
  • Crystal recommendation block filtered by zodiac sign on a collection page
  • Dream symbol lookup or an I-Ching oracle in a journal-store sidebar
  • Location and timezone autocomplete on any birth-data form (city to latitude, longitude, IANA zone)

Rung 2: App Proxy, when the key has to stay on the server

Shopify gives you four places to plug in: the App Proxy (storefront-relative URLs that route to your backend), theme app extensions (Liquid blocks merchants drop into the editor), checkout extensions (sandboxed UI in the funnel), and the Admin API plus webhooks for post-purchase flows. The pattern is always the same: keep the key on a server, return JSON or Liquid through the proxy, cache aggressively.

What you need

  1. A RoxyAPI key. Get one on the pricing page.
  2. A Shopify Partners account, plus either app development access on a development store or theme code access on a Shopify plan that allows custom code.

If you have never built a Shopify app, install the Shopify CLI and run shopify app init once. The React Router template scaffolds a backend you can drop a RoxyAPI fetch into, and it ships an authentication API that verifies app proxy requests for you, so the hand-rolled check below is only needed outside that template.

Step 1, keep the key safe

Three places, pick whichever matches your setup.

Most secure. The app runs on your own server (Vercel, Fly, Railway, Render, Cloudflare Workers). The key lives in the platform secrets store and never reaches the browser.

  1. In the app project, set ROXY_API_KEY=... in the platform environment (Vercel project settings, Fly secrets, Cloudflare Worker bindings).
  2. In your handler, read process.env.ROXY_API_KEY and send it as the X-API-Key header on every RoxyAPI call.

Never embed a secret key in theme.liquid, in theme JavaScript, in a custom theme section, in a metafield, in a script tag, or in a checkout UI extension. All of those ship to the browser. The App Proxy is the boundary: anything that needs RoxyAPI data goes through your backend, and your backend holds the key. A widget in a Custom Liquid section is the one exception, and it uses a publishable key that is built for exactly that.

Step 2, configure the proxy

In shopify.app.toml at the root of your app:

[access_scopes]
scopes = "write_app_proxy"

[app_proxy]
url = "https://your-app.example.com/proxy"
prefix = "apps"
subpath = "roxy"

prefix must be one of a, apps, community, or tools. subpath is up to 30 characters of letters, numbers, underscores, and hyphens, and may not be admin, services, password, or login. Run shopify app deploy. Requests to https://merchant-store.myshopify.com/apps/roxy/anything now proxy to https://your-app.example.com/proxy/anything.

Changes to url take effect immediately on every store that has the app. Changes to prefix and subpath apply only to new installations, because merchants can customize both per store, so change them on an already-installed dev store from the store admin or by reinstalling.

Step 3, ship a feature

Here is the full flow for a tarot card-of-the-day block. The card is seeded by Shopify customer ID so each customer gets one stable card per day across browsers.

// server.js, deploys to Vercel, Fly, Railway, or any Node host
import { Hono } from 'hono';
import crypto from 'node:crypto';

const app = new Hono();
const ROXY_BASE = 'https://roxyapi.com/api/v2';
const SHOPIFY_API_SECRET = process.env.SHOPIFY_API_SECRET;
const ROXY_API_KEY = process.env.ROXY_API_KEY;

function verifyAppProxySignature(requestUrl) {
  const params = new URL(requestUrl).searchParams;
  const signature = params.get('signature');
  if (!signature) return false;
  params.delete('signature');
  // Unencoded, sorted, concatenated. A repeated parameter joins its values on a comma.
  const message = [...new Set(params.keys())]
    .sort()
    .map((key) => `${key}=${params.getAll(key).join(',')}`)
    .join('');
  const computed = crypto
    .createHmac('sha256', SHOPIFY_API_SECRET)
    .update(message)
    .digest('hex');
  const a = Buffer.from(computed, 'utf8');
  const b = Buffer.from(signature, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.get('/proxy/tarot/daily', async (c) => {
  if (!verifyAppProxySignature(c.req.url)) {
    return c.json({ error: 'invalid signature' }, 401);
  }
  const query = new URL(c.req.url).searchParams;
  const seed = query.get('logged_in_customer_id') || query.get('shop');
  const res = await fetch(`${ROXY_BASE}/tarot/daily`, {
    method: 'POST',
    headers: {
      'X-API-Key': ROXY_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ seed: String(seed) }),
  });
  if (!res.ok) return c.json({ error: 'upstream' }, 502);
  const data = await res.json();
  c.header('Cache-Control', 'public, max-age=3600');
  return c.json(data);
});

export default app;

Shopify adds logged_in_customer_id to every proxied request and signs it, so the browser never has to send a customer ID and cannot forge one. The browser only ever talks to your-store.myshopify.com/apps/roxy/.... RoxyAPI never sees the customer browser, and the customer browser never sees the RoxyAPI key.

Customer account daily horoscope

The customer account page is a good place to surface a daily reading. You have customer.id and any saved metafields (birth date, zodiac sign) available in Liquid.

{% comment %} sections/customer-horoscope.liquid {% endcomment %}
{% if customer %}
  {% assign sign = customer.metafields.astrology.zodiac_sign | default: 'aries' %}
  <div id="roxy-horoscope" data-sign="{{ sign }}">
    <p>Loading your daily horoscope...</p>
  </div>
  <script>
    fetch('/apps/roxy/horoscope/{{ sign }}/daily')
      .then((r) => r.json())
      .then((data) => {
        document.getElementById('roxy-horoscope').innerHTML = `
          <h3>${data.sign} for ${data.date}</h3>
          <p>${data.overview}</p>
          <p><strong>Love:</strong> ${data.love}</p>
          <p><strong>Career:</strong> ${data.career}</p>
          <p>Lucky number ${data.luckyNumber}, lucky color ${data.luckyColor}.</p>
        `;
      });
  </script>
{% endif %}

The proxy handler for this route is the same shape as the tarot one. Verify the signature, forward GET /astrology/horoscope/{sign}/daily to RoxyAPI with the X-API-Key header, return JSON.

Post-checkout numerology email via webhook

The cleanest way to trigger RoxyAPI after a sale is the orders/paid webhook. Subscribe in shopify.app.toml:

[[webhooks.subscriptions]]
topics = ["orders/paid"]
uri = "https://your-app.example.com/webhooks/orders-paid"

The handler reads the order, looks up the customer birth date metafield, calls POST /numerology/life-path, and emails the report. That endpoint takes year, month, and day as integers, all required.

app.post('/webhooks/orders-paid', async (c) => {
  // Verify the Shopify webhook HMAC first (omitted for brevity).
  const order = await c.req.json();
  const birthDate = order.customer?.metafields?.numerology?.birth_date;
  if (!birthDate) return c.json({ ok: true });
  const [year, month, day] = birthDate.split('-').map(Number);
  const res = await fetch(`${ROXY_BASE}/numerology/life-path`, {
    method: 'POST',
    headers: {
      'X-API-Key': ROXY_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ year, month, day }),
  });
  const lifePath = await res.json();
  // Send the report with your email provider of choice.
  return c.json({ ok: true });
});

Add the next endpoint

Same pattern, new route on the app server, new path and body. The ROXY_API_KEY env var is shared.

Gotchas

  • App proxy signature verification. Every proxied request carries a signature query parameter: a hex-encoded SHA-256 HMAC of the other parameters, unencoded, sorted, and concatenated, keyed by your app shared secret. Skip the check and an attacker can spoof logged_in_customer_id and read any customer result. Do not assume the parameter list is fixed either, since Shopify adds new ones. The exact spec is at shopify.dev, Authenticate app proxies.
  • App proxy response Content-Type. Liquid rendering requires Content-Type: application/liquid on the response, and JSON consumed by JavaScript requires application/json. The wrong type fails quietly: a Liquid response served as JSON renders as raw braces, and a JSON response served as Liquid renders inside the theme template. Set the header explicitly on every proxy response.
  • App proxy strips many headers. Shopify drops Cookie and Set-Cookie along with several others from proxy requests and responses, so cookies cannot carry proxy auth. The signature parameter is the source of truth.
  • Theme JavaScript cannot call RoxyAPI with a secret key. A browser fetch from a theme block to https://roxyapi.com/... with a secret key in the JS leaks it to every visitor. Route through /apps/roxy/..., which is same-origin and proxies to your backend, or use a widget with a publishable key.
  • Checkout UI extensions are sandboxed. They run in a Remote DOM sandbox with a bundle size cap and restricted network access, so arbitrary fetch to external URLs is not the model. For checkout-time RoxyAPI calls, use a backend webhook (orders/paid, checkouts/create) or a Shopify Function.
  • Customer ID seeds. Use customer.id as the seed for endpoints that accept one (tarot daily and draw, I-Ching daily, angel-number daily). Same customer, same day, same result across browsers. Add the date to vary daily, add cart.token to vary per checkout.
  • Rate limits. Shopify caps app proxy requests per shop, and RoxyAPI enforces its own monthly quota on the key. Cache at the proxy layer (Cloudflare Workers KV, Vercel KV, Upstash Redis, or an in-memory map for a single-region app). The handlers above set Cache-Control: public, max-age=3600 on the way back to the browser.
  • Timezone. Prefer IANA strings (America/New_York, Europe/London) in birth-data payloads. A decimal offset like 5.5 is accepted but cannot handle daylight saving. Use GET /location/search?q=london to resolve a city name to latitude, longitude, and IANA zone in one call.
  • Errors. A success is clean JSON with no wrapper. A failure is { error, code }, and a 400 adds issues[] listing every field problem at once. Retry only on 429 and 5xx.

What to build next

  • The widgets guide covers rung 1 in full: every delivery mode, languages, and the error fix list.
  • The tarot guide and astrology guide cover endpoint ordering for commerce-friendly experiences.
  • The Next.js integration is the right move when your app server outgrows a single file.
  • The SDK guide is the typed reference for TypeScript or Python app servers.
  • Browse the API reference for every endpoint across 18+ domains.