# Use RoxyAPI with Shopify

> 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](https://shopify.com) store in under 20 minutes. The API key never touches the storefront.

Shopify gives you four clean 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. Roxy slots into all four. The pattern is always the same: keep the key on a server, return JSON or Liquid through the proxy, cache aggressively.

## 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 widget on the customer account page bound to a saved birth date
- Western natal chart product page that generates the chart at checkout completion
- Vedic kundli generator with PDF emailed after `orders/paid` webhook
- Numerology Life Path calculator as a pre-purchase product configurator
- Crystal recommendation block filtered by zodiac sign on a collection page
- I-Ching hexagram oracle accessed via storefront search
- Dream symbol lookup widget in a journal-store sidebar
- Angel number daily message in the customer account header
- Biorhythm chart for membership-app stores tracking date of birth
- Location and timezone autocomplete on any birth-data form (city to latitude, longitude, IANA zone)

## What you need, 30 seconds

1. A Roxy API key. Get one on the [pricing page](/pricing).
2. A [Shopify Partners](https://partners.shopify.com) account, plus either app development access on a development store or theme code access on a Shopify plan that allows custom code (Basic and above).
3. Ten minutes.

**Tip: If you have never built a Shopify app, install the [Shopify CLI](https://shopify.dev/docs/api/shopify-cli) and run `shopify app init` once. The React Router template scaffolds an Express-like backend you can drop a Roxy fetch into.**

## Step 1, connect your first endpoint

Three places to keep the Roxy key safe. Pick whichever matches your setup.


### Custom Shopify app, environment variable

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, etc.).
2. In your handler, read `process.env.ROXY_API_KEY` and pass it as the `X-API-Key` header on every Roxy call.

### Theme App Extension, App Proxy snippet

Easiest if you already have a Shopify app. The theme block calls `/apps/roxy/...` on the storefront. Shopify forwards the request to your app server, which holds the key.

1. Generate a theme app extension: `shopify app generate extension --type theme_app_extension`.
2. Create a Liquid block under `blocks/` that calls the App Proxy URL (no key in Liquid, no key in JavaScript).
3. The app server reads the key from environment variables, calls Roxy, returns JSON or Liquid.

### Shopify CLI app deploy, shop-scoped secret

For multi-shop apps where the key is per-merchant. Store the key in your app database keyed by `shop` domain, retrieve it inside the proxy handler.

This is the right pattern when distributing as a public Shopify app. Each merchant brings their own Roxy key during onboarding and you store it encrypted at rest.


**Warning: Never embed the API key in `theme.liquid`, in any theme JavaScript file, 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 Roxy data goes through your backend, your backend holds the key.**

Now configure the proxy. In `shopify.app.toml` at the root of your app:

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

Allowed `prefix` values are `a`, `apps`, `community`, or `tools`. The `subpath` is up to 30 characters of letters, numbers, underscores, hyphens. After this config, requests to `https://merchant-store.myshopify.com/apps/roxy/anything` proxy to `https://your-app.example.com/proxy/anything`.

Add `write_app_proxy` to your access scopes and run `shopify app deploy`.

## Step 2, ship a useful 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.


### App proxy handler (Hono on a server)

```javascript
// 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(query) {
  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', SHOPIFY_API_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)) {
    return c.json({ error: 'invalid signature' }, 401);
  }
  const customerId = query.logged_in_customer_id || query.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(customerId) }),
  });
  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;
```

### Theme app extension block (Liquid + JS)

```liquid
{% comment %} blocks/tarot-daily.liquid {% endcomment %}
<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
// assets/tarot-daily.js
document.addEventListener('DOMContentLoaded', async () => {
  const el = document.getElementById('roxy-tarot-daily');
  if (!el) return;
  const customerId = el.dataset.customerId || 'guest';
  const res = await fetch(`/apps/roxy/tarot/daily?customer=${customerId}`, {
    headers: { Accept: 'application/json' },
  });
  if (!res.ok) {
    el.innerHTML = '<p>Could not load today card.</p>';
    return;
  }
  const data = await res.json();
  el.innerHTML = `
    <h3>${data.card.name}${data.card.reversed ? ' (Reversed)' : ''}</h3>
    <img src="${data.card.imageUrl}" alt="${data.card.name}" />
    <p>${data.dailyMessage}</p>
  `;
});
```

### 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 browser only ever talks to `your-store.myshopify.com/apps/roxy/...`. Roxy never sees the customer browser, the customer browser never sees the Roxy key.

### Customer account daily horoscope

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

```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 Roxy with the `X-API-Key` header, return JSON.

### Post-checkout numerology email via webhook

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

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

```javascript
app.post('/webhooks/orders-paid', async (c) => {
  // Verify 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 via Resend, Postmark, SendGrid, or Shopify Email API.
  return c.json({ ok: true });
});
```

## Step 3, scale to the full surface

You have one block working. Adding the next 130 endpoints across all 10 domains is the same pattern: copy the snippet, change the URL, change the handler name, the `ROXY_API_KEY` env var is shared. Three places to pick the next one:

- **[API reference](/api-reference)** has a pre-filled test key. Try a call in the browser, copy the curl, translate into a Hono or Express route on your app server.
- **Domain guides** for which endpoints to call in what order:
  - [Western Astrology](/docs/guides/astrology), [Vedic Astrology](/docs/guides/vedic-astrology), [Tarot](/docs/guides/tarot), [Numerology](/docs/guides/numerology), [I-Ching](/docs/guides/iching), [Dreams](/docs/guides/dreams), [Crystals](/docs/guides/crystals), [Angel Numbers](/docs/guides/angel-numbers)
- **Most-used endpoints** that fit Shopify stores: [`POST /tarot/daily`](/api-reference#tag/tarot/POST/tarot/daily), [`POST /tarot/spreads/three-card`](/api-reference#tag/tarot/POST/tarot/spreads/three-card), [`GET /astrology/horoscope/{sign}/daily`](/api-reference#tag/western-astrology/GET/astrology/horoscope/{sign}/daily), [`POST /astrology/natal-chart`](/api-reference#tag/western-astrology/POST/astrology/natal-chart), [`POST /vedic-astrology/birth-chart`](/api-reference#tag/vedic-astrology/POST/vedic-astrology/birth-chart), [`POST /numerology/life-path`](/api-reference#tag/numerology/POST/numerology/life-path), [`GET /crystals/zodiac/{sign}`](/api-reference#tag/crystals-and-healing-stones/GET/crystals/zodiac/{sign}), [`GET /dreams/symbols`](/api-reference#tag/dreams/GET/dreams/symbols), [`GET /angel-numbers/numbers/{number}`](/api-reference#tag/angel-numbers/GET/angel-numbers/numbers/{number}), [`GET /location/search`](/api-reference#tag/location-and-timezone/GET/location/search).

## Where to put the code

Four options, roughly in order of distribution reach:

1. **Custom Shopify app.** Best for a single store you control. Set up an App Proxy, host on Vercel or Fly, store the Roxy key in env vars, ship features in days. Can use Shopify Functions for advanced checkout logic.
2. **Public Shopify app.** Best for distribution. List on the Shopify App Store, charge merchants for the integration, store per-merchant Roxy keys in your app database. Same App Proxy pattern, multiplied.
3. **Theme App Extension blocks.** Lightweight option when no app exists yet. Liquid blocks plus an App Proxy still requires a backend, so you cannot fully escape having a server somewhere.
4. **Standalone Hono or Express backend with App Proxy.** The simplest path if you are not a Shopify-native developer. Spin up a tiny server, point a Shopify app at it, all the storefront knows is `/apps/roxy/...`.

Avoid putting Roxy calls inside theme JavaScript with the key inlined. That always ends with a leaked key and a quota burn.

## Gotchas

- **App proxy signature verification.** Every proxied request from Shopify carries a `signature` query parameter. Sort the remaining query parameters alphabetically by key, concatenate as `key=value` (no `&` separator between pairs), compute `HMAC-SHA256` keyed by your app shared secret, hex-encode, and constant-time compare to `signature`. If you skip this step, attackers spoof `logged_in_customer_id` and the proxy returns data for any customer. The exact spec is at [shopify.dev, Authenticate app proxies](https://shopify.dev/docs/apps/build/online-store/app-proxies/authenticate-app-proxies).
- **App proxy response Content-Type.** Liquid rendering requires `Content-Type: application/liquid` on the response. JSON consumed by JavaScript requires `application/json`. The wrong type fails silently: a Liquid response served as JSON renders as raw braces, a JSON response served as Liquid renders inside the theme template wrapper. Set the header explicitly on every proxy response.
- **App proxy strips many headers.** Shopify drops `Set-Cookie`, `Cookie`, `Server`, `Date`, `X-Powered-By`, and others from the proxy response. Do not rely on cookies for proxy auth. Use the `signature` parameter as the source of truth.
- **Theme app extensions cannot fetch external URLs from JS.** Browser `fetch` from a theme block to `https://roxyapi.com/...` exposes the key (if it is in the JS) and hits CORS issues. Always route through `/apps/roxy/...`, which is same-origin and proxies to your backend.
- **Checkout UI extensions are sandboxed.** Checkout UI extensions run in a Remote DOM sandbox, the bundle size cap is 64 KB, network access is restricted, and arbitrary `fetch` to external URLs is not the model. For checkout-time Roxy calls, use a backend webhook (`orders/paid`, `checkouts/create`) or a Shopify Function, not a UI extension `fetch`.
- **Customer ID seeds.** Use `customer.id` directly as the `seed` for endpoints that accept one (tarot daily, tarot draw, I-Ching daily, angel-number daily). Same customer, same day, same result across browsers and sessions. Use `customer.id + date` to vary daily, use `customer.id + cart.token` to vary per-checkout.
- **Rate limits.** Shopify caps app proxy requests per shop, Roxy enforces its own monthly quota on the API key. Cache aggressively at the proxy layer (Cloudflare Workers KV, Vercel KV, Upstash Redis, even an in-memory Map for single-region apps). The proxy handler examples set `Cache-Control: public, max-age=3600` on responses, which Cloudflare and Shopify both honor on the way back to the browser.
- **Timezone.** Prefer IANA strings (`"America/New_York"`, `"Asia/Kolkata"`) in birth-data payloads. Decimal offsets like `5.5` are accepted but do not handle daylight saving. The server resolves IANA to the DST-correct offset for the request date. Use [`GET /location/search?q=mumbai`](/api-reference#tag/location-and-timezone/GET/location/search) to resolve a city name to latitude, longitude, and IANA zone in one call.

## What to build next

- The [tarot guide](/docs/guides/tarot) and [astrology guide](/docs/guides/astrology) cover endpoint ordering for commerce-friendly experiences.
- The [Next.js integration](/docs/integrations/nextjs) is the right move when your Shopify app server outgrows a single file.
- The [SDK guide](/docs/sdk) is the typed reference for TypeScript or Python app servers.
- Browse the [API reference](/api-reference) for every endpoint across all 10 domains.
