Insights by OmkarUrja

Integration guide

Integration guide

How to call Urja — the Visual API — from a product. Everything public is served under https://urja.insightsbyomkar.com/api/v1/*. No SDK required for the core flows — most endpoints return image/svg+xml and drop straight into an <img>. For JSON endpoints, a typed URL builder lives at lib/client/ in this repo (or the scaffolded urja-client package under packages/urja-client/).

The legacy host urja.insightsbyomkar.com is kept as a permanent alias — existing integrations keep working. New integrations should use urja.insightsbyomkar.com.

Auth

Every authenticated route is guarded by a scoped JWT. Keys are minted through three channels, none of them shared-password:

  • Staff minting — role-based via

commandcenter's Urja portal (any owner / operator / support profile). Use for test keys, partner grants, or one-off provisioning.

  • System mintingPOST /api/internal/provision-key, HMAC-signed

with SSO_SHARED_SECRET. The consumer's Stripe webhook uses this path to auto-provision keys on subscription activation.

  • Self-serve rotation — customers rotate their own keys at

studio.insightsbyomkar.com/account/api-keys/[keyId]/rotate, which HMAC-calls the internal endpoint under the hood.

Include the key on each request in the Authorization header:

Authorization: Bearer <your-jwt>

Scopes

Keys are minted with one or more scopes. An umbrella scope grants every sub-scope within the track.

ScopeGrants
adminEvery /admin route — key minting, revocation, registry inspection.
critic:v1Umbrella. Read + use on the critic track.
critic:v1:readGET /rubrics, GET /references (free tier).
critic:v1:usePOST /critique — metered / billable.
character:v1Umbrella. Use on the character track.
character:v1:useGET /character — SVG render.
studio:v1Umbrella. Use across effects, scene, palette, tokens, fonts, motion.
studio:v1:useEvery studio-scoped endpoint.

Most integrations want a single umbrella key with critic:v1:use + character:v1:use + studio:v1:use.

Local dev

When VISUAL_API_SECRET is unset, the guard runs in open mode and every request passes. Useful for exploring the API from a laptop; never commit a deploy that leaves it unset in production.


Caching

  • Most GET endpoints return `Cache-Control: public, max-age=86400,

s-maxage=604800, immutable` — 1 day browser / 7 days CDN.

  • Responses are deterministic per the full query string. Two requests

with identical params return byte-identical bodies, so CDN caching is safe.

  • When you need a one-off variation (hero image for a new campaign,

say), change the seed param — same character, different deterministic render.


Rate limits

Every authenticated response carries the following headers. Use them to pace your client before you hit a 429.

HeaderMeaning
X-RateLimit-Limit-MinutePer-key requests allowed per 60-second window.
X-RateLimit-Remaining-MinuteRequests left in the current window.
X-RateLimit-Reset-MinuteUnix-seconds when the minute window resets.
X-RateLimit-Limit-DayPer-key daily quota.
X-RateLimit-Remaining-DayRequests left in the current day.
X-RateLimit-Reset-DayUnix-seconds when the daily quota resets.
Retry-AfterOnly on 429s — seconds to wait before retrying.

Limits are per-API-key and come from the plan the key was minted under. Plans are documented in lib/api/plans.ts; contact support to change tier. Public endpoints (flags, ornaments, pricing-card scene, umbrella-hero scene) are not rate-limited per key but are protected by edge-layer IP throttling against abuse.


OpenAPI spec

Machine-readable contract at GET /api/v1/openapi.json. OpenAPI 3.1, covers every public endpoint, documents the x-api-key security scheme, and includes parameter enums for scene type, token version, flag country, and so on. Codegen with openapi-typescript is the fast path to a fully-typed client.

The source JSON is committed at docs/openapi.json; the route reads it at request time so edits ride alongside route changes.


Self-info — GET /api/v1/me

Authenticated. Returns a single JSON document describing the key that made the request — plan tier, quota ceilings, current usage in both rate-limit windows, scopes, issuance + expiry, and any owner label attached at mint time.

{
  "service": "urja",
  "key": {
    "id": "key_abc123",
    "mode": "jwt",
    "owner": "Acme Corp",
    "scopes": ["studio:v1:use", "character:v1:use"],
    "issuedAt": 1745452800,
    "expiresAt": 1776988800
  },
  "plan": {
    "id": "pro",
    "label": "Pro",
    "quotaPerMin": 300,
    "quotaPerDay": 50000
  },
  "quotas": { "perMinute": 300, "perDay": 50000 },
  "usage": {
    "backend": "supabase",
    "perMinute": { "limit": 300, "used": 12, "remaining": 288, "resetAt": 1745456400 },
    "perDay":    { "limit": 50000, "used": 482, "remaining": 49518, "resetAt": 1745539200 }
  }
}

Used by the unified account dashboard on studio.insightsbyomkar.com/account/api-keys and by urja-client's fetchSelf() helper. Cache-Control: no-store, private.


Health

GET /api/health returns a small JSON payload:

{
  "status": "ok",
  "service": "urja",
  "version": "0.4.0",
  "studioApiVersion": "1.4.0",
  "startedAt": "2026-04-24T00:00:00.000Z",
  "now": "2026-04-24T12:34:56.789Z"
}

Public, no scope required, Cache-Control: no-store. Safe to poll from uptime monitors and customer CI.


Errors

JSON endpoints return structured errors:

{
  "error": {
    "status": 401,
    "code": "missing_auth",
    "message": "Authorization header required."
  }
}

SVG endpoints that fail auth return the same JSON shape with a matching HTTP status. Validation failures on image endpoints return 400 with an available: field listing valid values so your integration can surface the exact enum to the user.


Endpoint reference

Character — GET /api/v1/character

Posable character render. Drop the URL into an <img>; CDN handles the rest.

ParamDefaultNotes
nameluckyCharacter slug. availableCharacters() in lib/character.
poseidleidle \resting \listening \beside-you.
moodsoft \attentive \warm \uncertain \joyful \quiet.
size240Integer px, clamped [24, 1200].
motionautoauto \static \breathing-only \full. Animated variants play via SMIL inside a raw <img>, zero JS.
labelderivedOverrides the aria-label on the <svg>.

Response: image/svg+xml. Scope: character:v1:use.

Scene — GET /api/v1/scene

Background + character composed into one SVG. The URL is the integration — no client-side stacking.

ParamDefaultNotes
characterluckyReuses the character track's catalogue.
poselistening
mood
backgroundauroraaurora \glow \grain \starfield \none.
paletteper-effect defaultaurora \ember \nebula \forest \ocean \rose.
seedderivedDeterministic seed for the effect. Override for campaign variations.
width960[200, 3840].
height540[160, 2160].
characterScale0.72Fraction of scene height the character occupies. [0.3, 0.95].
aligncentercenter \left \right.

Response: image/svg+xml. Scope: studio:v1:use. Unknown values degrade to defaults — a typo in the src still renders something.

GET /api/v1/scene?type=pricing-card

Backdrop-only atmosphere for /pricing tiles. Product-tinted, non-animated, public (no scope — the pricing page is unauthenticated by design). No character; the tile's own copy renders on top.

ParamDefaultNotes
typeMust be pricing-card to hit this branch.
productluckylucky \netra \astrology-api \visual-api \critic \character. Picks the element palette (fire / water / earth / air / nebula / rose).
tierprofree \pro \max. Modulates blob density and chromatic bloom so the upgrade story reads at a glance.
width600[240, 1200].
height400[160, 900].
seed<product>-<tier>Override for campaign variations.

GET /api/v1/scene?type=umbrella-hero

Ecosystem-scale backdrop for the apex pricing hero. All six element palettes (fire / water / earth / air / nebula / rose) layered as soft radial blobs with a dark vignette pulling focus to the centre where headline copy sits. Public.

ParamDefaultNotes
typeMust be umbrella-hero.
width1600[640, 3840].
height800[320, 2160].
seedumbrella-heroJitters blob anchors deterministically.
motionstaticstatic \drift. Drift adds a slow 40–80s cycle per blob.
intensity0.32Alpha ceiling for the composite — lower for a quieter backdrop. [0.1, 0.6].
grain0.08Film-grain amount; 0 disables. [0, 0.3].

Effects — GET /api/v1/effects/{aurora,glow,grain,starfield}

Ambient animated SVG. Zero-JS runtime — SMIL animation plays inside a raw <img>. See each endpoint for its own parameters:

  • aurora — palette, seed, width, height, speed, grain
  • glow — palette, width, height, intensity, chromatic, pulse
  • grain — kind (film \| paper \| halftone), seed, width, height, intensity, tone
  • starfield — palette, kind (stars \| dust \| snow \| confetti), seed, width, height, density, speed

Scope: studio:v1:use.

Illustrate — GET /api/v1/illustrate

Vector primitives: glyphs, ornaments, viz, chart wheels.

/api/v1/illustrate?type=<kind>&slug=<id>

Supported type values:

TypeSlugs
zodiacaries, taurus, gemini, … (12 signs)
planetsun, moon, mercury, …, chiron (14 bodies)
aspectconjunction, opposition, trine, square, sextile
icon36 utility icons — nav, actions, UI states, commerce. See ICON_SLUGS.
flag30 countries. Uses country=<CC> + `style=monoline\full instead of slug`. 24×16 viewBox.
ornamentcompass-rose, brass-divider, corner-mark, starfield, moon-phase, chart-ring, blob, ppp-banner, blob-cluster, silhouette, wordmark, gradient-mesh, particles, orbit-loader, pulse, wave, constellation
patternaspect-pattern (with name= variant)
vizsparkline, progress-arc, donut, bar, heatmap, diverging-bar
chart-wheelGET returns empty preview with ascendant=. POST renders a full chart — see POST /api/v1/illustrate below.

Common params: size, color, stroke, plus builder-specific extras. Scope: public (no auth required for SVG output).

The ppp-banner ornament adds two extras:

  • accent — palette-key alias that resolves to a brand hex (brass,

midnight, ink, parchment, muted, rose). Overrides color.

  • tintColor — optional CSS colour for a faint centre wash behind the

banner copy. Omitted by default.

Default size is 800×80 (width clamped [240, 2000], height clamped [40, 200]).

Chart wheel — POST /api/v1/illustrate

Renders a full natal chart. Request body (JSON):

{
  "type": "chart-wheel",
  "houses": [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330],
  "planets": [
    { "body": "sun", "longitude": 145.2, "retrograde": false },
    { "body": "moon", "longitude": 78.5 }
  ],
  "aspects": [
    { "from": "sun", "to": "moon", "aspect": "trine" }
  ],
  "palette": { "...": "..." },
  "caption": "Natal chart"
}

Response: image/svg+xml with the same cache headers as the GET side.

Critic — POST /api/v1/critic/critique

Cross-provider taste auditor. Scores a rendered output against a named rubric + reference set.

{
  "rubricId": "character-figure",
  "referenceSetId": "canine-animated",
  "output": { "kind": "image", "mediaType": "image/png", "base64": "..." }
}

Returns axis-by-axis scores, weighted total, and a list of failings with severity + proposed fixes. See docs/changelog/critic-v1.md for the full contract.

List rubrics: GET /api/v1/critic/rubrics. Fetch one: GET /api/v1/critic/rubrics/{id}. Same shape for reference sets.

Scope: critic:v1:use for critique calls, critic:v1:read for listing.

Studio — palette / tokens / fonts / motion

  • GET /api/v1/palette — all palette groups (or ?group= for one). ?format=css returns a CSS variable block; default is JSON.
  • GET /api/v1/tokens — design tokens (spacing, radius, shadow, etc.). Append &version=<x.y.z> to pin the surface (see Pinning below).
  • GET /api/v1/fonts — font stack definitions + type scale. See the self-hosted @font-face flow below.
  • GET /api/v1/motion — easing curves + duration tokens + motion primitives. See the motion CSS + runtime flow below.

Scope: studio:v1:use for JSON responses. ?format=css responses are public so hosts can load them via <link rel="stylesheet"> — no JWT required on the CSS path.

Pinning — ?version=<x.y.z>

Every /api/v1/tokens response accepts an optional version pin. Valid values are listed in SUPPORTED_TOKEN_VERSIONS and echoed in the JSON catalog under supportedVersions. Unknown values return 400 with the allow-list. The pinned version appears in the x-studio-api-version response header and in the first line of the CSS output.

Pin when shipping to production — it insulates you from silent token renames in future /api/v1/tokens surfaces.

Fonts — self-hosted @font-face

Drop this into the host layout:

<link
  rel="preload"
  href="https://urja.insightsbyomkar.com/api/v1/fonts/display/display-400-normal-latin.woff2"
  as="font" type="font/woff2" crossorigin="anonymous" />
<link
  rel="stylesheet"
  href="https://urja.insightsbyomkar.com/api/v1/fonts?format=css&families=display,sans" />

The CSS emits one @font-face per registered variant plus a :root block exposing --ifv-font-display and --ifv-font-sans for hosts to alias into their own token system. Initial families: Fraunces (display) and Inter (sans), both variable WOFF2 with latin + latin-ext subsets, both OFL-1.1. Each variant file is served at /api/v1/fonts/:family/:slug with Cache-Control: immutable for one year.

Motion — CSS primitives + runtime

Three selection modes via query string:

/api/v1/motion?format=css&set=base            # fade-up, parallax-drift, breath
/api/v1/motion?format=css&set=all             # base + 6 UI primitives
/api/v1/motion?format=css&names=fade-up,menu-drop

UI primitives live under .m-<name> classes (m-hover-lift, m-stagger-in, m-accordion-expand, m-modal-enter, m-toast-slide, m-menu-drop); each bundles its own prefers-reduced-motion fallback. Character-animation primitives keep their historical .lucky-<name> prefix.

Optional ESM runtime at /api/v1/motion/runtime.js:

import { stagger, onScroll, ready } from "https://urja.insightsbyomkar.com/api/v1/motion/runtime.js";

ready(() => {
  stagger(".product-grid > *", { each: 60, animation: "fade-up" });
  onScroll(".reveal-on-scroll", { animation: "fade-up", rootMargin: "-10% 0px" });
});

Zero deps, browser-only. Non-goals: gesture handling, springs, shared-layout — out of scope for v1.

A single <script> tag + a custom element drops the ecosystem navigation onto any host page. Same component across every Insights by Omkar property.

<script src="https://urja.insightsbyomkar.com/api/v2/nav/ecosystem-nav.js"></script>
<iby-ecosystem-nav
  current="studio"
  user-email="kavya@example.com"
  user-display-name="Kavya Rao"
  return-url="https://studio.insightsbyomkar.com/dashboard">
  <nav slot="subnav"><!-- product-local tabs --></nav>
</iby-ecosystem-nav>

Attributes (all optional except current): user-email, user-display-name, user-avatar-initial, search (on|off), return-url.

Events bubble + composed, so listen on window: ecosystem-nav:search (debounced 120ms, detail.query), ecosystem-nav:search-open, ecosystem-nav:waffle-open, ecosystem-nav:waffle-close, ecosystem-nav:signout.

Keyboard: ⌘K / Ctrl+K anywhere focuses the search input. Escape dismisses panels. Host implements its own command palette by listening for ecosystem-nav:search — the nav stays pure-presentational.

v1 bundle at /api/v1/nav/ecosystem-nav.js continues to serve the thin product bar until every consumer has migrated. Same tag name, additive attrs; see docs/changelog/nav-v2.md for the full upgrade.

Public, no scope required.


Using the client URL builders

The repo ships with type-safe URL constructors at lib/client/. Import in your consumer code:

import {
  buildCharacterUrl,
  buildSceneUrl,
  buildEffectUrl,
  buildIllustrateUrl,
} from "@/lib/client";

// Hero image — one URL, SMIL animation plays inside <img>
const heroUrl = buildSceneUrl({
  pose: "listening",
  mood: "attentive",
  background: "aurora",
  width: 1200,
  height: 600,
});

// Static character card — poster frame, no motion
const avatarUrl = buildCharacterUrl({
  pose: "beside-you",
  mood: "warm",
  size: 240,
  motion: "static",
});

// Standalone effect — backdrop for a section divider
const dividerBg = buildEffectUrl("starfield", {
  palette: "nebula",
  width: 1600,
  height: 320,
  density: 120,
});

// Zodiac glyph
const ariesGlyph = buildIllustrateUrl({
  type: "zodiac",
  slug: "aries",
  size: 64,
  color: "#d4a550",
});

Every builder accepts a baseUrl override for local development:

const url = buildSceneUrl({
  baseUrl: "http://localhost:3000",
  pose: "listening",
});

Types exported from @/lib/client cover every enum in the public contract (CharacterPose, SceneBackground, EffectKind, etc.) so typos fail at compile time instead of shipping a broken image URL.


Per-track deep dives

This page is the cross-track primer. Each track has its own deep-dive with full parameter tables, usage patterns, and quirks:

Versioning

Each track follows SemVer independently. Minor versions add characters, poses, palettes, effects; major versions change defaults, remove values, or break response shape. Breaking changes within a major ship under /v2, not in place.

Track changelogs live under docs/changelog/:

  • docs/changelog/character-v1.md
  • docs/changelog/critic-v1.md
  • docs/changelog/effects-v1.md
  • docs/changelog/illustrate-v1.md
  • docs/changelog/nav-v2.md
  • docs/changelog/studio-v1.md

See docs/API-VERSIONING.md for the full policy.


Rate limits + billing

Rate-limiting runs through @upstash/ratelimit; the in-memory fallback is used when UPSTASH_REDIS_REST_URL is unset (dev only). Billed endpoints (currently only POST /api/v1/critic/critique) record usage against the authenticated key's account. Billing integration with Stripe is scoped behind the consumer-pays milestone — contact for a billable key until then.