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 hosturja.insightsbyomkar.comis kept as a permanent alias — existing integrations keep working. New integrations should useurja.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 minting —
POST /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.
| Scope | Grants |
|---|---|
admin | Every /admin route — key minting, revocation, registry inspection. |
critic:v1 | Umbrella. Read + use on the critic track. |
critic:v1:read | GET /rubrics, GET /references (free tier). |
critic:v1:use | POST /critique — metered / billable. |
character:v1 | Umbrella. Use on the character track. |
character:v1:use | GET /character — SVG render. |
studio:v1 | Umbrella. Use across effects, scene, palette, tokens, fonts, motion. |
studio:v1:use | Every 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
GETendpoints 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.
| Header | Meaning |
|---|---|
X-RateLimit-Limit-Minute | Per-key requests allowed per 60-second window. |
X-RateLimit-Remaining-Minute | Requests left in the current window. |
X-RateLimit-Reset-Minute | Unix-seconds when the minute window resets. |
X-RateLimit-Limit-Day | Per-key daily quota. |
X-RateLimit-Remaining-Day | Requests left in the current day. |
X-RateLimit-Reset-Day | Unix-seconds when the daily quota resets. |
Retry-After | Only 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.
| Param | Default | Notes | |||||
|---|---|---|---|---|---|---|---|
name | lucky | Character slug. availableCharacters() in lib/character. | |||||
pose | idle | idle \ | resting \ | listening \ | beside-you. | ||
mood | — | soft \ | attentive \ | warm \ | uncertain \ | joyful \ | quiet. |
size | 240 | Integer px, clamped [24, 1200]. | |||||
motion | auto | auto \ | static \ | breathing-only \ | full. Animated variants play via SMIL inside a raw <img>, zero JS. | ||
label | derived | Overrides 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.
| Param | Default | Notes | |||||
|---|---|---|---|---|---|---|---|
character | lucky | Reuses the character track's catalogue. | |||||
pose | listening | ||||||
mood | — | ||||||
background | aurora | aurora \ | glow \ | grain \ | starfield \ | none. | |
palette | per-effect default | aurora \ | ember \ | nebula \ | forest \ | ocean \ | rose. |
seed | derived | Deterministic seed for the effect. Override for campaign variations. | |||||
width | 960 | [200, 3840]. | |||||
height | 540 | [160, 2160]. | |||||
characterScale | 0.72 | Fraction of scene height the character occupies. [0.3, 0.95]. | |||||
align | center | center \ | 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.
| Param | Default | Notes | |||||
|---|---|---|---|---|---|---|---|
type | — | Must be pricing-card to hit this branch. | |||||
product | lucky | lucky \ | netra \ | astrology-api \ | visual-api \ | critic \ | character. Picks the element palette (fire / water / earth / air / nebula / rose). |
tier | pro | free \ | pro \ | max. Modulates blob density and chromatic bloom so the upgrade story reads at a glance. | |||
width | 600 | [240, 1200]. | |||||
height | 400 | [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.
| Param | Default | Notes | |
|---|---|---|---|
type | — | Must be umbrella-hero. | |
width | 1600 | [640, 3840]. | |
height | 800 | [320, 2160]. | |
seed | umbrella-hero | Jitters blob anchors deterministically. | |
motion | static | static \ | drift. Drift adds a slow 40–80s cycle per blob. |
intensity | 0.32 | Alpha ceiling for the composite — lower for a quieter backdrop. [0.1, 0.6]. | |
grain | 0.08 | Film-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, grainglow— palette, width, height, intensity, chromatic, pulsegrain— kind (film\|paper\|halftone), seed, width, height, intensity, tonestarfield— 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:
| Type | Slugs | |
|---|---|---|
zodiac | aries, taurus, gemini, … (12 signs) | |
planet | sun, moon, mercury, …, chiron (14 bodies) | |
aspect | conjunction, opposition, trine, square, sextile | |
icon | 36 utility icons — nav, actions, UI states, commerce. See ICON_SLUGS. | |
flag | 30 countries. Uses country=<CC> + `style=monoline\ | full instead of slug`. 24×16 viewBox. |
ornament | compass-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 | |
pattern | aspect-pattern (with name= variant) | |
viz | sparkline, progress-arc, donut, bar, heatmap, diverging-bar | |
chart-wheel | GET 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=cssreturns 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.
Nav — shared ecosystem top-bar
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:
/docs/character— parametric character renderer (Lucky)./docs/scene— character + background composition, plus public pricing-card and umbrella-hero branches./docs/effects— aurora, glow, grain, starfield./docs/illustrate— glyphs, icons, flags, ornaments, chart wheels, viz./docs/motion— easing / duration tokens, UI primitives, ESM runtime./docs/tokens—--ds-*semantic layer, version pinning./docs/fonts— self-hosted @font-face CSS + WOFF2 binaries./docs/palette— five colour-preset groups./docs/nav— shared ecosystem top-bar web component./docs/critic— cross-provider taste auditor.
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.mddocs/changelog/critic-v1.mddocs/changelog/effects-v1.mddocs/changelog/illustrate-v1.mddocs/changelog/nav-v2.mddocs/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.
