:::note
**TL;DR**
- Add a tarot card of the day, daily horoscope, kundli generator, and post-checkout numerology email to any Shopify store in one afternoon, with the API key on a server and zero key exposure to the storefront.
- Shopify App Proxy is the boundary: requests to `/apps/roxy/...` on the storefront forward to a backend that holds the key, verifies the HMAC-SHA256 signature, and calls Roxy.
- Use Shopify `customer.id` directly as the `seed` on `POST /tarot/daily` so the same shopper sees a stable card all day across browsers and devices.
- Build this with the [Tarot API](/products/tarot-api "production-ready Tarot API with daily cards, spreads, and yes-no oracle") in under 20 minutes.
:::

A tarot card of the day inside a Shopify storefront is a small feature with an oversized retention effect. Repeat visits land on a fresh reading, the deck is the brand, and the reading itself becomes a reason to log in tomorrow. The harder question is where to put the code, where the API key lives, and how to keep the same card stable for a returning shopper. Shopify App Proxy answers all three, and the pattern generalises to daily horoscope on the customer account, kundli on a product page, and a numerology Life Path report attached to a paid order. The deep technical reference is the [Shopify integration guide](/docs/integrations/shopify "step-by-step Shopify App Proxy and theme app extension guide for RoxyAPI").

## How do I add tarot readings to a Shopify store?

Add tarot readings by routing storefront requests through a Shopify App Proxy to a small backend that calls Roxy and returns JSON, with the API key stored as an environment variable on the backend. The storefront block fetches `/apps/roxy/tarot/daily`, the backend verifies the App Proxy signature, calls `POST /tarot/daily` with the shopper `customer.id` as the `seed`, and renders the response. No theme JavaScript ever sees the Roxy key, the App Proxy is the auth boundary, and the same shopper receives the same daily card on every device.

Three places hold code: a Hono or Express handler that owns `process.env.ROXY_API_KEY`, a theme app extension block that emits Liquid plus a fetcher script, and the `shopify.app.toml` that wires the App Proxy to the backend route. The backend authenticates Shopify and calls Roxy, the block renders inside the merchant theme, the toml maps the storefront prefix to the backend URL.

Ready to build this? Get the [Tarot API](/products/tarot-api "production-ready Tarot API with daily cards, spreads, and yes-no oracle") with daily card seeded readings, three-card spreads, Celtic Cross, and yes-no oracle in one subscription. [See pricing](/pricing "RoxyAPI pricing and plan tiers").

## The App Proxy pattern: request flow, signature verification, response shape

Shopify App Proxy is a server-side reverse proxy from a configurable storefront prefix to a backend URL, with HMAC-SHA256 signature verification, automatic injection of the logged-in customer ID, and Liquid rendering on demand. A request to `https://merchant.myshopify.com/apps/roxy/tarot/daily` forwards to your backend at `https://your-app.example.com/proxy/tarot/daily` with `shop`, `logged_in_customer_id`, `path_prefix`, `timestamp`, and `signature` query parameters appended. The signature is HMAC-SHA256 of the other parameters, sorted by key, joined as `key=value` with no separator, hex-encoded, keyed by the app shared secret.

Allowed `prefix` values are exactly four: `a`, `apps`, `community`, `tools`. The `subpath` is up to 30 characters of letters, numbers, underscores, hyphens, and may not be `admin`, `services`, `password`, or `login`. Add `write_app_proxy` to access scopes and configure in `shopify.app.toml`.

:::tabs

### App Proxy handler (Hono)

```javascript
import { Hono } from 'hono';
import crypto from 'node:crypto';

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

function verifyAppProxySignature(query, secret) {
  const { signature, ...rest } = query;
  if (!signature) return false;
  const sortedMessage = Object.keys(rest)
    .sort()
    .map((k) => `${k}=${Array.isArray(rest[k]) ? rest[k].join(',') : rest[k]}`)
    .join('');
  const computed = crypto
    .createHmac('sha256', secret)
    .update(sortedMessage)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(computed),
    Buffer.from(signature)
  );
}

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

export default app;
```

### curl preview

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

The response is `{ date, seed, card: { id, name, arcana, position, reversed, keywords, meaning, imageUrl }, dailyMessage }`. The `seed` and `date` together determine the card, so the same shopper gets the same card across browsers all day.

:::

:::warning App Proxy signature verification is not optional
Shopify forwards every request from the storefront with a `signature` query parameter that proves the request came from Shopify and the parameters were not tampered with. Without verification, an attacker can call your backend directly and pass any `logged_in_customer_id`, returning data scoped to a customer they do not own. The algorithm: take every query parameter except `signature`, sort the keys alphabetically, concatenate as `key=value` with no separator between pairs, compute HMAC-SHA256 keyed by the app shared secret, hex-encode, and constant-time compare to the `signature` value. The signature only proves provenance, not authorisation. Always verify that `logged_in_customer_id` matches the data the request asks for. Source: [shopify.dev, Authenticate app proxies](https://shopify.dev/docs/apps/build/online-store/app-proxies/authenticate-app-proxies "Shopify App Proxy authentication and signature verification").
:::

## Daily tarot card on the customer account (theme app extension calling App Proxy)

A theme app extension block is the cleanest place to render the daily card on a customer-facing surface, because the merchant adds the block in the theme editor without touching theme code. The block is a Liquid file under `blocks/` that emits a placeholder div carrying `{{ customer.id }}` as a data attribute, plus an asset script that fetches the App Proxy URL and replaces the placeholder. Direct fetch from a theme block to roxyapi.com would expose the key or hit CORS, and it bypasses the proxy boundary. Always route through `/apps/roxy/...`, which is same-origin and proxies to your backend.

```liquid
<div id="roxy-tarot-daily" data-customer-id="{{ customer.id }}">
  <p>Loading your card of the day...</p>
</div>
<script src="{{ 'tarot-daily.js' | asset_url }}" defer></script>

{% schema %}
{ "name": "Tarot card of the day", "target": "section", "settings": [] }
{% endschema %}
```

```javascript
const el = document.getElementById('roxy-tarot-daily');
const customerId = el.dataset.customerId || 'guest';
const r = await fetch(`/apps/roxy/tarot/daily?customer=${customerId}`);
const { card, dailyMessage } = await r.json();
el.innerHTML = `<h3>${card.name}${card.reversed ? ' (Reversed)' : ''}</h3>
  <img src="${card.imageUrl}" alt="${card.name}" />
  <p>${dailyMessage}</p>`;
```

:::tip Use customer.id directly as the seed
The Roxy `POST /tarot/daily` endpoint takes an optional `seed` parameter. Pass Shopify `customer.id` as a string and the same shopper receives the identical card every time the page loads on any device, all day long. Combine with the date for daily rotation. Combine with `cart.token` instead for a per-checkout seed that varies between sessions but stays stable inside one checkout. The same `seed` plus same `date` always produces the same card. Reference: [seeded tarot reading patterns](/blogs/reproducible-tarot-readings-seeded-api "reproducible tarot readings with seeded random draws").
:::

The same shape works for daily horoscope on the customer account page. Read the zodiac sign from a customer metafield, render a placeholder, fetch `/apps/roxy/horoscope/{sign}/daily` from the proxy, display the response. Roxy `GET /astrology/horoscope/{sign}/daily` returns `sign`, `date`, `overview`, `love`, `career`, `health`, `finance`, `advice`, `luckyNumber`, `luckyColor`, `compatibleSigns`, `activeTransits`, `moonSign`, `moonPhase`, and `energyRating`.

## Post-checkout numerology email (orders/paid webhook + Life Path endpoint)

A checkout UI extension cannot make this call. Checkout extensions run inside a Remote DOM sandbox separated from the page, the bundle ceiling is 64 KB, and arbitrary fetch to external URLs is not the model. The right shape for post-purchase work is a webhook subscription on `orders/paid` that hits a backend route, reads the order, looks up the customer birth date metafield, calls Roxy, and emails the report.

Subscribe in `shopify.app.toml`:

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

The handler verifies the Shopify webhook HMAC, parses the order payload, splits the birth date into year, month, day integers, and calls `POST /numerology/life-path`:

```javascript
app.post('/webhooks/orders-paid', async (c) => {
  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 r = await fetch(`${ROXY_BASE}/numerology/life-path`, {
    method: 'POST',
    headers: {
      'X-API-Key': process.env.ROXY_API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ year, month, day }),
  });
  const lifePath = await r.json();
  // emit to Resend / Postmark / Shopify Email API
  return c.json({ ok: true });
});
```

The response carries `number`, `calculation`, `type`, `hasKarmicDebt`, `karmicDebtNumber`, `karmicDebtMeaning`, and a `meaning` object with `title`, `keywords`, `description`, `strengths`, `challenges`, `career`, `relationships`, `spirituality`. That is enough for a personalised email, a PDF, or a customer-account report. The same webhook can branch on order tags to also call `POST /vedic-astrology/birth-chart` for a kundli product or `POST /astrology/natal-chart` for a Western chart product.

## Where this fits in Shopify (custom app vs theme extension vs Shopify Function vs checkout extension)

Four extensibility surfaces, four different jobs. Most spiritual storefront features land on App Proxy plus a theme app extension block. A checkout extension cannot fetch external APIs, a Shopify Function is for discount and shipping logic, theme code cannot hold a key. Pick the surface from the work the feature does, not from where it appears on the page.

| Surface | What it is | Calls external APIs from JS | Holds the API key | Best for |
|---|---|---|---|---|
| Custom app + App Proxy | Backend route at `/apps/{prefix}/{path}` on the storefront | Yes (server-side fetch) | Yes (env var on backend) | Tarot card, daily horoscope, kundli, any Roxy call |
| Theme app extension block | Liquid block plus asset script in the theme | Same-origin only via App Proxy | No (key never reaches the theme) | Customer-facing render of proxy responses |
| Shopify Function | Sandboxed Wasm runtime for discount, shipping, payment, delivery customisation | No (no network) | No (no network) | Per-cart pricing logic, not API integrations |
| Checkout UI extension | Remote DOM sandbox with 64 KB bundle ceiling, restricted runtime | No (no arbitrary fetch) | No | Read cart, render UI inside checkout funnel |

Backend route does the work, theme block renders the result, webhooks handle post-purchase, Shopify Functions handle discounts. Roxy slots into the first three. For deeper reference, the Shopify documentation on [theme app extensions](https://shopify.dev/docs/apps/build/online-store/theme-app-extensions "theme app extension framework documentation") and [checkout UI extensions](https://shopify.dev/docs/api/checkout-ui-extensions "checkout UI extensions API and sandboxing constraints") is the source of truth.

## Common gotchas (signature spoofing, JSON vs Liquid, sandbox limits, customer.id stability)

A small set of failure modes catches most Shopify Roxy integrations on first deploy. None require code changes beyond the App Proxy handler, but each is silent until you debug it.

- **App Proxy signature spoofing.** Without HMAC-SHA256 verification, anyone can call the backend directly and spoof `logged_in_customer_id`. Treat the signature as the only proof of provenance, then re-check that the customer ID matches the data scope.
- **Wrong Content-Type breaks Liquid.** Responses with `Content-Type: application/liquid` are rendered against the theme; everything else returns to the client as-is. JSON consumed by JavaScript needs `application/json`. Set the header explicitly.
- **App Proxy strips many response headers.** Shopify drops `Set-Cookie`, `Cookie`, `Server`, `Date`, `X-Powered-By`, and others on the way back. Do not rely on cookies for proxy auth. The signature parameter is the source of truth.
- **Theme JS cannot fetch roxyapi.com directly.** Direct fetch from a theme asset leaks the key, hits CORS, and bypasses the proxy boundary. Always route through `/apps/roxy/...`.
- **Checkout extensions cannot call external APIs.** The Remote DOM sandbox blocks arbitrary network access and caps the bundle at 64 KB. Use `orders/paid` webhooks for post-purchase Roxy work.
- **customer.id is stable per-shop, not cross-shop.** A guest user has no `customer.id`, so fall back to the `shop` parameter or a cart token. The same `customer.id` produces the same daily card on the same date for that shop only.

## FAQ

**How do I add tarot readings to a Shopify store?**

Configure a Shopify App Proxy in `shopify.app.toml` that forwards `/apps/roxy/{path}` on the storefront to a backend handler you control. The backend verifies the App Proxy HMAC-SHA256 signature, calls Roxy `POST /tarot/daily` with the Shopify `customer.id` as the `seed`, and returns JSON. A theme app extension block on the storefront fetches the App Proxy URL same-origin and renders the card. The Roxy API key never reaches the storefront.

**Do I need a Shopify app for this?**

Yes, App Proxy is an app-level feature configured in `shopify.app.toml`, so a Shopify Partner app is required even for a single-store install. The app does not have to be public or listed on the Shopify App Store. A custom app installed on the merchant store is enough. Theme code alone cannot hold an API key safely, and a public storefront fetch to roxyapi.com would either leak the key or hit CORS.

**Is my API key safe in a theme app extension?**

No, never put the key in a theme app extension, theme JavaScript, theme Liquid, a metafield, a script tag, or a checkout extension. All of those ship to the browser. The App Proxy is the boundary: the storefront fetches `/apps/roxy/...`, your backend reads the key from `process.env.ROXY_API_KEY`, calls Roxy server-side, and returns JSON to the storefront. The key only lives on the backend.

**Can checkout extensions call external APIs?**

No, checkout UI extensions run in a Remote DOM sandbox with restricted network access and a 64 KB bundle cap, so arbitrary fetch to external URLs is not the model. For checkout-time Roxy work, subscribe to the `orders/paid` webhook and call Roxy from the webhook handler. The webhook fires after payment is captured, so the customer order data is final and available for personalisation.

**How do I keep daily tarot stable per customer?**

Pass the Shopify `customer.id` directly as the `seed` parameter on `POST /tarot/daily`. The Roxy daily endpoint produces the same card for the same `seed` plus same date, so a logged-in shopper sees an identical card on every device for the full day. The card rotates at midnight UTC. Use `customer.id` plus `cart.token` instead for a per-checkout seed that varies between visits but stays stable inside one checkout flow.

## Conclusion

Shopify App Proxy is a clean boundary for any Roxy call: tarot card of the day, daily horoscope on the customer account, kundli product page, post-checkout numerology email. The same pattern handles all 10 domains behind one [Tarot API](/products/tarot-api "production-ready Tarot API with daily cards, spreads, and yes-no oracle") and one [Astrology API](/products/astrology-api "production-ready Western Astrology API with horoscopes, natal charts, and transits") subscription. For the deep technical reference and the full per-snippet build, the [Shopify integration guide](/docs/integrations/shopify "step-by-step Shopify App Proxy and theme app extension guide") is the source of truth, and accuracy claims trace back to the public [methodology page](/methodology "RoxyAPI test methodology and ephemeris validation against NASA JPL Horizons").