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

Wix astrology widget with a Velo backend

Put a natal chart tool, a daily horoscope, a tarot spread, or a Life Path calculator on any Wix or Wix Studio site. Paste an embed for a visitor-facing reading, or wire Velo to a backend web module for member-gated features with a secret key on the server.

Two rungs, and most sites only need the first. A copy-paste widget draws its own form, asks your visitor for their birth details, and renders the reading, with no code panel and no Dev Mode. Velo is the step up: a secret key held in the Secrets Manager, member-gated or customer-scoped readings, and full control over the markup.

Rung 1: paste a widget, no code

Every embeddable widget has a prefilled snippet on the widgets gallery. Paste your key there once and every snippet on the page fills itself in.

Get a publishable key

A publishable key (pk_) is built for the browser and is safe to leave in a page. It cannot read your account, and you lock it to your own domain.

  1. Open your account API keys (Account, API Keys).
  2. Click New key, choose Publishable and Live.
  3. Under Allowed origins, add the host your site is served from, for example yourdomain.com. Add the www. form and your *.wixsite.com preview host too if you publish on both.
  4. Copy the pk_live_... value and paste it into the box at the top of the widgets gallery.

Add the embed in the editor

In the Wix Editor or Wix Studio, click Add Elements, click Embed Code, then Popular Embeds, and pick one of two elements:

Takes a URL and shows the page in a window on yours. Click Enter Website Address and paste the hosted embed link with your own key on the end, then click Apply.

https://roxyapi.com/embed/natal-chart?pk=pk_live_YOUR_KEY

RoxyAPI serves that page, so the reading is fetched from our own domain and the only host that has to be on your key allowlist is your Wix site. Resize the element to give the reading room; a natal chart wants roughly 600px of height.

Only HTTPS URLs and snippets render, on both elements. Swap natal-chart for any slug on the gallery: horoscope-card, tarot-spread, vedic-kundli, numerology-card, bodygraph, moon-phase and more. Full delivery-mode reference and error fixes: the widgets guide.

Never put a secret key in a page

A secret sk_ key belongs on a server. The widget refuses one client side and shows an error instead of a reading. Publishable keys and secret keys live in the same account, so mint a publishable one and use that.

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

Reach for Velo when you need a secret key server side, member-gated or customer-scoped readings, results written to a collection, or markup you control element by element. The Wix layout is three pieces:

  1. Secrets Manager holds the RoxyAPI key, encrypted, readable only from backend code.
  2. Backend web module (a .web.js file) calls the API with the key.
  3. Page code imports the web method and binds the result to elements.

What you need

  1. A RoxyAPI key. Get your API key.
  2. A Wix site with Dev Mode on. In the Wix Editor click Dev Mode, then Turn on Dev Mode. In Wix Studio the code panel is already there.

Run the quickstart curl once in a terminal before you bring the key into Wix. A successful JSON response confirms the key itself is good.

Store the key

From the Wix Editor or Wix Studio, open Developer Tools in the code panel and click Secrets Manager under Security. From the dashboard, open Developer Tools and click Secrets Manager. Click Add Secret at the top right, name it ROXY_API_KEY, paste the key from your account API keys, and save. Changes apply immediately with no republish.

Write the web module

Wix Studio sidebar, Backend, +, New .web.js file. Name it roxy.web.js.

// backend/roxy.web.js
import { Permissions, webMethod } from 'wix-web-module';
import { fetch } from 'wix-fetch';
import { secrets } from 'wix-secrets-backend.v2';
import { elevate } from 'wix-auth';

const ROXY_BASE = 'https://roxyapi.com/api/v2';
const readSecret = elevate(secrets.getSecretValue);

async function roxy(path, body) {
  const { value: apiKey } = await readSecret('ROXY_API_KEY');
  const res = await fetch(`${ROXY_BASE}${path}`, {
    method: body ? 'POST' : 'GET',
    headers: body
      ? { 'X-API-Key': apiKey, 'Content-Type': 'application/json' }
      : { 'X-API-Key': apiKey },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await res.json();
  // A success is clean JSON. A failure is { error, code, doc_url }, and a 400 adds issues[].
  if (!res.ok) throw new Error(data.error || `RoxyAPI ${res.status}`);
  return data;
}

export const searchCities = webMethod(Permissions.Anyone, (q) =>
  roxy(`/location/search?q=${encodeURIComponent(q)}`)
);

export const getNatalChart = webMethod(Permissions.Anyone, (birthData) =>
  roxy('/astrology/natal-chart', birthData)
);

What each import is doing:

  • fetch from wix-fetch is the outbound HTTP client that runs on the backend.
  • secrets.getSecretValue from wix-secrets-backend.v2 reads the encrypted secret and returns { value }. It needs elevate from wix-auth, because reading a secret is an admin action and a web method runs as the visitor.
  • webMethod(Permissions.Anyone, ...) exposes the function to page code. Swap in Permissions.SiteMember or Permissions.Admin to restrict it.

Web module files end in .web.js. Older guides show the legacy .jsw format, and so do older wix-secrets-backend examples that call getSecret; both have been superseded.

Display a tarot spread

For a custom tarot page, call POST /tarot/spreads/three-card through the backend module when the visitor requests a reading. Bind the returned positions to a Wix repeater: each item supplies its position name, interpretation, and drawn card. Keep the secret key on the backend and apply the same member permissions as your other gated readings. The tarot guide covers card fields and rendering.

Ship a natal chart form

The natal chart form needs latitude, longitude, and timezone. Never ask a visitor for coordinates: let them type a city and resolve the three values with searchCities first. Sign-based horoscopes do not need a birthplace; check each endpoint for its required inputs.

import { searchCities, getNatalChart } from 'backend/roxy.web';

let place = null;

$w.onReady(() => {
  $w('#cityInput').onInput(async () => {
    const q = $w('#cityInput').value;
    if (q.length < 3) return;
    const { cities } = await searchCities(q);
    $w('#cityDropdown').options = cities.map((c) => ({
      label: `${c.city}, ${c.province}, ${c.country}`,
      value: JSON.stringify(c),
    }));
  });

  $w('#cityDropdown').onChange(() => {
    place = JSON.parse($w('#cityDropdown').value);
  });

  $w('#submitButton').onClick(async () => {
    if (!place) {
      $w('#statusText').text = 'Pick your birth city from the list first.';
      return;
    }
    $w('#submitButton').disable();
    $w('#statusText').text = 'Calculating your chart...';
    const d = $w('#birthDate').value;
    const pad = (n) => String(n).padStart(2, '0');
    try {
      const chart = await getNatalChart({
        date: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`,
        time: `${$w('#birthTime').value}:00`,
        latitude: place.latitude,
        longitude: place.longitude,
        timezone: place.timezone,
      });
      const byName = (name) => chart.planets.find((p) => p.name === name);
      $w('#sunSign').text = byName('Sun').sign;
      $w('#moonSign').text = byName('Moon').sign;
      $w('#risingSign').text = chart.ascendant.sign;
      $w('#resultBox').show();
      $w('#statusText').text = '';
    } catch (err) {
      $w('#statusText').text = err.message;
      console.error(err);
    } finally {
      $w('#submitButton').enable();
    }
  });
});

Each city from /location/search carries city, province, country, latitude, longitude, and an IANA timezone, so one pick fills the whole trio.

Cache what every visitor shares

A daily horoscope is the same for every visitor for the day, and most lookup data (dream symbols, hexagram meanings, angel numbers) never changes. Wix web methods cache the return value per argument set.

export const getDailyHoroscope = webMethod(
  Permissions.Anyone,
  (sign) => roxy(`/astrology/horoscope/${sign}/daily`),
  {
    cache: {
      tags: ['roxy-horoscope'],
      ttl: 3600,
    },
  },
);

tags is required: leave it out and nothing is cached. ttl is in seconds and defaults to a week, so set it deliberately. Each sign caches independently, because the cache key is the argument set.

Never cache a user-specific result: a personal natal chart, numerology for one name, transits for one birth moment. Cache only what is identical for every visitor for a known period.

Add the next endpoint

Same pattern, new webMethod in the same file, new path and body. roxy() and the secret are already there.

Gotchas

  • Backend only. Never call RoxyAPI from page code with a secret key. Page code ships to the browser. Route every call through a .web.js module with the key in the Secrets Manager, or use a publishable key and a widget.
  • Timezone. Prefer the IANA string that /location/search returns (America/New_York, Europe/London). A decimal offset like 5.5 is accepted but cannot handle daylight saving; the IANA name resolves to the correct offset for the birth date.
  • Errors. A success is clean JSON with no wrapper. A failure is { error, code, doc_url }, and a 400 adds issues[] listing every field problem at once. Retry only on 429 and 5xx.
  • Some sites refuse to be framed. Wix says so plainly for the Embed a site element, and it is a policy the embedded site sets. RoxyAPI embeds allow the domains on your key allowlist, so if the frame stays blank, check that the site host is on the key.
  • HTTPS only. Both embed elements drop an http:// URL or snippet on the published site.
  • Preview against live. Preview runs in a sandbox that sometimes masks errors. If a call works in Preview and fails on the published site, check the Wix Developer Console and confirm the secret exists on the live site.
  • Element IDs. $w('#id') only resolves inside $w.onReady or an event handler. Calling a web method before onReady fires can reference elements that do not exist yet.

What to build next