- Docs
- Integrations
- Shopify
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 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.idso 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/paidwebhook - 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
- A Roxy API key. Get one on the pricing page.
- 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 (Basic and above).
- Ten minutes.
If you have never built a Shopify app, install the 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.
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.
- In the app project, set
ROXY_API_KEY=...in the platform environment (Vercel project settings, Fly secrets, Cloudflare Worker bindings, etc.). - In your handler, read
process.env.ROXY_API_KEYand pass it as theX-API-Keyheader on every Roxy call.
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.
- Generate a theme app extension:
shopify app generate extension --type theme_app_extension. - Create a Liquid block under
blocks/that calls the App Proxy URL (no key in Liquid, no key in JavaScript). - The app server reads the key from environment variables, calls Roxy, returns JSON or Liquid.
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.
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:
[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.
// 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;
{% 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 %}
// 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 -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.
{% 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:
[[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.
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 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:
- Most-used endpoints that fit Shopify stores:
POST /tarot/daily,POST /tarot/spreads/three-card,GET /astrology/horoscope/{sign}/daily,POST /astrology/natal-chart,POST /vedic-astrology/birth-chart,POST /numerology/life-path,GET /crystals/zodiac/{sign},GET /dreams/symbols,GET /angel-numbers/numbers/{number},GET /location/search.
Where to put the code
Four options, roughly in order of distribution reach:
- 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.
- 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.
- 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.
- 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
signaturequery parameter. Sort the remaining query parameters alphabetically by key, concatenate askey=value(no&separator between pairs), computeHMAC-SHA256keyed by your app shared secret, hex-encode, and constant-time compare tosignature. If you skip this step, attackers spooflogged_in_customer_idand the proxy returns data for any customer. The exact spec is at shopify.dev, Authenticate app proxies. - App proxy response Content-Type. Liquid rendering requires
Content-Type: application/liquidon the response. JSON consumed by JavaScript requiresapplication/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 thesignatureparameter as the source of truth. - Theme app extensions cannot fetch external URLs from JS. Browser
fetchfrom a theme block tohttps://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
fetchto 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 extensionfetch. - Customer ID seeds. Use
customer.iddirectly as theseedfor endpoints that accept one (tarot daily, tarot draw, I-Ching daily, angel-number daily). Same customer, same day, same result across browsers and sessions. Usecustomer.id + dateto vary daily, usecustomer.id + cart.tokento 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=3600on 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 like5.5are accepted but do not handle daylight saving. The server resolves IANA to the DST-correct offset for the request date. UseGET /location/search?q=mumbaito resolve a city name to latitude, longitude, and IANA zone in one call.
What to build next
- The tarot guide and astrology guide cover endpoint ordering for commerce-friendly experiences.
- The Next.js integration is the right move when your Shopify 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 all 10 domains.