# Build a Vastu property report

> Ship a report that reads a plot, judges the main door, checks every room, draws the Vastu Purusha Mandala over the plan and prints the verse behind each verdict. Four RoxyAPI calls, one server route, one web component. Time to ship: 60 minutes.

A Vastu report is the feature a property listing, an interior design tool or a consultation platform charges for, and every free checker on the web produces one. What none of them produce is a report that can defend itself: which lines rest on a classical verse, which rest on later practice, and what the verse actually says. This tutorial builds that report from the four geometry calls in the [Vastu domain](/docs/guides/vastu). No birth data, no location lookup, no drawing upload: a rectangle, a facing, a door and a list of rooms in, a printable report out.

## What you can build

- Listing page Vastu cards for real estate platforms, with a score and the reasons behind it
- Consultation reports a practitioner prints, with the classical half and the practice half labelled
- Floor plan editors with a live mandala overlay and a door position slider
- Buyer facing plot checks before a purchase, from the survey dimensions alone
- Chat assistants that answer "is my kitchen in the right corner" over Remote MCP

## Prerequisites

1. A RoxyAPI key from [/account](/account).
2. The plot as `width` (east to west) and `depth` (north to south), in feet or metres, plus the `facing` of the front.
3. Where the main door sits, as a fraction along the front, and each room as a type plus the quarter it occupies.
4. A backend you control (Next.js route, Vercel function, Cloudflare Worker, Bun server). The key never reaches the browser.

**Tip: Nothing in this report needs a birth chart or a city. The only Vastu call that takes a place is the griha pravesh date search, covered as an optional last step.**

## Install


### npm
```bash
npm install @roxyapi/sdk
```

### Python
```bash
pip install roxy-sdk
```

### PHP
```bash
composer require roxyapi/sdk
```

### CDN (render layer only)
```html
<script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@0/dist/cdn/roxy-ui.js" defer></script>
```

## Suggested project location

```
vastu-report/
├── app/
│   ├── api/
│   │   └── report/route.ts        # server route: four calls, one payload
│   ├── report/page.tsx            # the report
│   └── layout.tsx
├── lib/
│   └── cite.ts                    # the source renderer from Step 6
└── .env.local                     # ROXY_API_KEY=...
```

Next.js 16, TypeScript, Tailwind. Substitute your stack of choice.

## Step 1: Model the input

One object drives all four calls. Coordinates run x east and y north, and a rectangle sent as `width` and `depth` is the box whose width is its east to west extent. The mandala is aligned to the compass, so `facing` says which side the front is on and never rotates the grid.

```json
{
  "plot": { "width": 30, "depth": 40, "unit": "feet" },
  "facing": "East",
  "doorPosition": 0.3,
  "rooms": [
    { "type": "kitchen", "direction": "Southeast" },
    { "type": "toilet", "direction": "Northeast" },
    { "type": "master-bedroom", "direction": "Southwest" },
    { "type": "puja", "direction": "Northeast" }
  ],
  "slopeLowDirection": "Northeast",
  "road": "East",
  "water": "Northeast"
}
```

`doorPosition` is a fraction from 0 to 1 along the facing side, counted from the corner the classical chapter starts that side at. To aim at pada n of the eight on a side, send its midpoint, (n - 0.5) / 8. A room can be sent as a `direction` or as a `polygon` of 3 to 16 points in plot coordinates, and an outline is read from its area centroid, so an L shaped room lands where its mass is.

## Step 2: The plot verdict

[`POST /vastu/plot`](/api-reference#tag/vastu/POST/vastu/plot) reads the ground: shape, proportion, ground level under two schools, standing water, extensions and cuts, the road. Verified operationId: `calculatePlotAnalysis`.


### curl
```bash
curl -X POST https://roxyapi.com/api/v2/vastu/plot \
  -H "X-API-Key: $ROXY_API_KEY" -H "Content-Type: application/json" \
  -d '{"plot":{"width":30,"depth":40},"facing":"East",
       "slopeLowDirection":"Northeast","road":"East","water":"Northeast"}'
```

### TypeScript SDK
```typescript
import { createRoxy } from '@roxyapi/sdk';
const roxy = createRoxy(process.env.ROXY_API_KEY!);

const { data: plot } = await roxy.vastu.calculatePlotAnalysis({
  body: {
    plot: { width: 30, depth: 40 },
    facing: 'East',
    slopeLowDirection: 'Northeast',
    road: 'East',
    water: 'Northeast',
  },
});

console.log(plot.orientation);            // { facing: "East", sitting: "West" }
console.log(plot.dimensions);             // { length: 40, breadth: 30, area: 1200, ratio: 1.333 }
console.log(plot.shape.verdict);          // "regular"
console.log(plot.ratio.verdict);          // "within-convention-band"
console.log(plot.slope.schools[0]);       // { school: "brihat-samhita", verdict: "not-stated", verse: "53.116", effect: "..." }
console.log(plot.slope.schools[1]);       // { school: "modern", verdict: "auspicious", verse: "", effect: "..." }
console.log(plot.water.effect);           // "Sons."
console.log(plot.road.auspiciousness);    // "auspicious"
```

### Python SDK
```python
import os
from roxy_sdk import create_roxy

roxy = create_roxy(os.environ['ROXY_API_KEY'])

plot = roxy.vastu.calculate_plot_analysis(
    plot={'width': 30, 'depth': 40}, facing='East',
    slope_low_direction='Northeast', road='East', water='Northeast',
)
print(plot['shape']['verdict'], plot['ratio']['verdict'])
for school in plot['slope']['schools']:
    print(school['school'], school['verdict'], school['verse'])
```

Read it like this. `shape.verdict`, `ratio.verdict`, `slope.schools[].verdict`, `water.auspiciousness` and `road.auspiciousness` are canonical English in every language, so branch and style on them. `effect` on each is the sentence to print. `slope.schools` always carries BOTH readings, because the classical verses and the modern teaching disagree on a slight rise to the east or north, and `slope.chosen` names the one you asked to lead with. A 30 by 40 site reads `within-convention-band`: the practice rule is that the longer side is at most twice the shorter, whichever way the plot lies.

## Step 3: The entrance

[`POST /vastu/entrance`](/api-reference#tag/vastu/POST/vastu/entrance) turns the plot and the door into one of the 32 perimeter padas, the devata of that square, the stated effect and the better padas on the same side. Verified operationId: `calculateEntrancePada`.

```typescript
const { data: entrance } = await roxy.vastu.calculateEntrancePada({
  body: { plot: { width: 30, depth: 40 }, facing: 'East', doorPosition: 0.3 },
});

console.log(entrance.pada);              // 3
console.log(entrance.ordinalOnSide);     // 3, the third of the eight on the East side
console.log(entrance.devata.name);       // "Jayanta"
console.log(entrance.effect);            // "Great wealth."
console.log(entrance.auspiciousness);    // "auspicious"
console.log(entrance.recommendedPadas);  // [3, 4]
console.log(entrance.source.verse);      // "72"
```

`recommendedPadas` only ever names positions on the same side, so a suggestion asks the owner to move a door, never a wall. On a south facing plot it comes back empty, because the chapter names no gain on that side, and the honest report says so rather than inventing a compromise.

To draw where a recommended pada sits on the plan, join two catalogues you fetch once and cache forever: the devata list maps a pada to its square, and the mandala from Step 5 maps a square to a centre point in plot coordinates.

```typescript
const { data: catalogue } = await roxy.vastu.listDevatas();

// pada -> devata id, for the 32 devatas that hold a perimeter square
const devataOfPada = new Map(
  catalogue.devatas
    .filter((d) => d.entrancePada !== undefined)
    .map((d) => [d.entrancePada, d.id]),
);

// square -> centre, from the projected mandala of Step 5
const centreOfPada = (pada: number) =>
  mandala.cells.find((c) => c.class === 'perimeter' && c.devata === devataOfPada.get(pada))?.center;

entrance.recommendedPadas.map(centreOfPada);
// [{ x: 28.33, y: 28.89 }, { x: 28.33, y: 24.44 }] on the 30 by 40 plot
```

## Step 4: The rooms

[`POST /vastu/rooms`](/api-reference#tag/vastu/POST/vastu/rooms) takes the same plot and facing plus 1 to 24 rooms, and returns a verdict per room with the quarters it belongs in, the quarters to keep it out of, a remedy where it is misplaced, and a composite score. Verified operationId: `calculateRoomCompliance`. The twelve room types are `puja`, `kitchen`, `master-bedroom`, `bedroom`, `living`, `dining`, `study`, `toilet`, `store`, `staircase`, `water-storage` and `entrance`.

```typescript
const { data: rooms } = await roxy.vastu.calculateRoomCompliance({
  body: {
    plot: { width: 30, depth: 40 },
    facing: 'East',
    rooms: [
      { type: 'kitchen', direction: 'Southeast' },
      { type: 'toilet', direction: 'Northeast' },
    ],
  },
});

console.log(rooms.rooms[0].verdict);   // "ideal"
console.log(rooms.rooms[0].remedy);    // undefined, an ideal room has nothing to fix
console.log(rooms.rooms[1].verdict);   // "avoid"
console.log(rooms.rooms[1].remedy);    // "Move the toilet to the north-west or the west. ..."
console.log(rooms.score);              // 66.7
console.log(rooms.scoring);            // { sourcedWeight: 2, conventionWeight: 1, idealPoints: 1, neutralPoints: 0.5, avoidPoints: 0 }
```

Three things make this a report rather than a scorecard. `verdict` is one of `ideal`, `acceptable` or `avoid`, canonical in every language. `remedy` is present only when there is something to do, so a card can show it without a condition of its own. And `scoring` publishes the weights the `score` was built from: a rule carrying a verse weighs twice a rule carrying practice, an ideal placement scores full, an acceptable one half, a placement to avoid nothing. Print the weights beside the number and the score explains itself.

**Warning: The score is a RoxyAPI composite, not a classical quantity. No verse gives a number. Show it as a summary of the rows under it, never as the finding.**

## Step 5: The mandala over the plan

[`POST /vastu/mandala`](/api-reference#tag/vastu/POST/vastu/mandala) projects the 81 pada grid over the plot and returns geometry in your own coordinates: every square with its devata and centre, the brahmasthan as a polygon, the marma points, the six vamsa diagonals and the nine atimarma crossings. Verified operationId: `generateMandala`.

```typescript
const { data: mandala } = await roxy.vastu.generateMandala({
  body: { plot: { width: 30, depth: 40 } },
});

console.log(mandala.cells.length);          // 81
console.log(mandala.brahmasthan.squares);   // [31, 32, 33, 40, 41, 42, 49, 50, 51]
console.log(mandala.brahmasthan.polygon);   // [{ x: 10, y: 13.33 }, { x: 20, y: 13.33 }, { x: 20, y: 26.67 }, { x: 10, y: 26.67 }]
console.log(mandala.brahmasthan.area);      // 133.33, in square feet
console.log(mandala.marma.areaEach);        // 1.85
console.log(mandala.atimarma);              // [23, 31, 33, 39, 41, 43, 49, 51, 59]
```

Two ways to draw it, shortest first.

### Option A: the drop-in web component

`<roxy-vastu-mandala>` from [`@roxyapi/ui`](/docs/ui) renders the grid with a devata on every square and the brahmasthan marked. Pass the mandala response for the projection, or the entrance response with `mode="entrance"` to light the door square with its effect. Fetch on your server and inline the unwrapped response, so the key stays out of the page.

```html
<roxy-vastu-mandala mode="mandala">
  <script type="application/json" class="roxy-data">
    { "grid": "81-pada", "cells": [ ... ], "brahmasthan": { ... }, "marma": { ... }, "vamsa": [ ... ], "atimarma": [ ... ], "sources": [ ... ] }
  </script>
</roxy-vastu-mandala>

<roxy-vastu-mandala mode="entrance">
  <script type="application/json" class="roxy-data">
    { "pada": 3, "side": "East", "square": 3, "cell": { "rowFromNorth": 3, "columnFromWest": 9 }, "devata": { "id": "jayanta", "name": "Jayanta", "padaCount": 2 }, "effect": "Great wealth.", "auspiciousness": "auspicious", "reading": "...", "recommendedPadas": [3, 4], "source": { ... }, "conventions": { "grid": "81-pada", "unit": "feet" } }
  </script>
</roxy-vastu-mandala>
```

Your server template writes the JSON in. Setting the `.data` property from JavaScript later always wins over the inlined JSON, so the same tag covers a static report and a live editor.

### Option B: your own SVG

The response is already geometry, so an overlay on a floor plan is thirty lines. The one trap is the axis: plot y runs north, SVG y runs down, so every point is flipped through the depth.

```typescript
type Point = { x: number; y: number };

export function mandalaSvg(
  plot: { width: number; depth: number },
  mandala: { cells: { square: number; class?: string; devataName?: string; center: Point }[];
             brahmasthan: { polygon: Point[] }; vamsa?: { from: Point; to: Point }[] },
  doorSquare?: number,
) {
  const { width, depth } = plot;
  const w = width / 9;                   // 81 pada grid, nine to a side
  const h = depth / 9;
  const Y = (y: number) => depth - y;    // north is up on the plot, down in SVG

  const cells = mandala.cells.map(
    (c) => `<rect x="${c.center.x - w / 2}" y="${Y(c.center.y) - h / 2}" width="${w}" height="${h}"
      class="${c.class}${c.square === doorSquare ? ' door' : ''}"><title>${c.devataName}</title></rect>`,
  );
  const brahma = mandala.brahmasthan.polygon.map((p) => `${p.x},${Y(p.y)}`).join(' ');
  const vamsa = (mandala.vamsa ?? []).map(
    (l) => `<line x1="${l.from.x}" y1="${Y(l.from.y)}" x2="${l.to.x}" y2="${Y(l.to.y)}" />`,
  );

  return `<svg viewBox="0 0 ${width} ${depth}" class="mandala">
    ${cells.join('')}
    <polygon points="${brahma}" class="brahmasthan" />
    ${vamsa.join('')}
  </svg>`;
}

// mandalaSvg({ width: 30, depth: 40 }, mandala, entrance.square)
```

Style `.perimeter`, `.innerRing`, `.innerCorner` and `.center` by the `class` each cell carries, and `.door` for the entrance square. Put the floor plan image behind the SVG at the same aspect ratio and the overlay lines up, because both are in the same plot units. For a plot sent as a `polygon`, take the cell size from its bounding box instead of `width` and `depth`, and use `withinPlot` to fade the squares that fall outside a cut corner.

## Step 6: One server route, four calls

The four calls share the plot and the facing and nothing depends on another, so run them together and return one payload.

```typescript
// app/api/report/route.ts
import { createRoxy } from '@roxyapi/sdk';

const roxy = createRoxy(process.env.ROXY_API_KEY!);

export async function POST(req: Request) {
  const input = await req.json();
  const { plot, facing } = input;

  const [plotVerdict, entrance, rooms, mandala] = await Promise.all([
    roxy.vastu.calculatePlotAnalysis({
      body: { plot, facing, slopeLowDirection: input.slopeLowDirection, road: input.road, water: input.water },
    }),
    roxy.vastu.calculateEntrancePada({ body: { plot, facing, doorPosition: input.doorPosition } }),
    roxy.vastu.calculateRoomCompliance({ body: { plot, facing, rooms: input.rooms } }),
    roxy.vastu.generateMandala({ body: { plot } }),
  ]);

  return Response.json({
    plot: plotVerdict.data,
    entrance: entrance.data,
    rooms: rooms.data,
    mandala: mandala.data,
  });
}
```

Geometry never changes, so the payload for a given input never changes either. Cache it on a hash of the request body with no expiry and a listing page pays for its report once. The [caching guide](/docs/guides/caching) covers the split between answers that never move and answers that move daily.

## Step 7: Print the citations

Every verdict in the four responses carries a `source` object with exactly two shapes, and `text` is the discriminator. When it is the literal `convention` there is no chapter or verse and `basis` says what the rule rests on. When it is a text name, `chapter`, `verse`, `translation`, `year` and `publicDomain` are present.

```typescript
// lib/cite.ts
export type Source = {
  text: string;
  chapter?: number;
  verse?: string;
  translation?: string;
  year?: number;
  publicDomain?: boolean;
  basis?: string;
};

export const cite = (s: Source) =>
  s.text === 'convention'
    ? `Practice: ${s.basis}`
    : `${s.text} ${s.chapter}.${s.verse} (${s.translation}, ${s.year})`;

export const classicalShare = (sources: Source[]) => {
  const classical = sources.filter((s) => s.text !== 'convention').length;
  return `${classical} of ${sources.length} verdicts rest on a verse; the rest are labelled practice`;
};
```

Feed it every verdict the report shows and print the sentence under the score:

```typescript
const sources = [
  report.plot.shape.source,
  report.plot.ratio.source,
  ...(report.plot.water ? [report.plot.water.source] : []),
  report.entrance.source,
  ...report.rooms.rooms.map((r) => r.source),
];
classicalShare(sources);
// "5 of 8 verdicts rest on a verse; the rest are labelled practice", for the Step 1 input
```

That one line is what a free checker cannot print. `publicDomain` is `true` on every Brihat Samhita citation, whose verses are quoted from an 1884 edition, so the citation is safe to reproduce in a client report.

## Step 8 (optional): Griha pravesh dates

A house warming date is the one part of the report that needs a place and a window. Resolve the city with [`GET /location/search`](/api-reference#tag/location-and-timezone/GET/location/search), then call [`POST /vastu/timing/griha-pravesh`](/api-reference#tag/vastu/POST/vastu/timing/griha-pravesh) (`findGrihaPraveshDates`) with a window of at most 93 days.

```typescript
const { data: loc } = await roxy.location.searchCities({ query: { q: 'Bangalore', limit: 1 } });
const city = loc.cities[0];

const { data: dates } = await roxy.vastu.findGrihaPraveshDates({
  body: {
    startDate: '2026-11-01',
    endDate: '2026-12-31',
    latitude: city.latitude,
    longitude: city.longitude,
    timezone: city.timezone,
  },
});

dates.days.map((d) => [d.date, d.quality, d.admittedBy]);
dates.leftToTheAstrologer; // the rules a date search cannot settle, published rather than dropped
```

Show `leftToTheAstrologer` in the report. The lagna rules and the eighth from the owner birth moon sign are judgements about a moment and a person, and a report that presents the dates as final is overstating them.

## Reply in the user language

Every call accepts `?lang=`. Pass `query: { lang: 'hi' }` in the TypeScript SDK or `lang='hi'` in Python, and `reading`, `effect`, `remedy`, `basis` and the slope prose come back in Hindi while `verdict`, `auspiciousness`, `side`, `zone` and `source.text` stay canonical English. The devata names on the mandala and the entrance are written in Devanagari under `hi` and romanised everywhere else, so key the overlay on `devata`, the id, and print `devataName`. This domain ships German, Spanish, French, Hindi, Portuguese, Russian and Turkish alongside the English source. Branch on the identifier, render the prose.

```typescript
const { data } = await roxy.vastu.calculateRoomCompliance({
  body: { plot: { width: 30, depth: 40 }, facing: 'East', rooms: [{ type: 'toilet', direction: 'Northeast' }] },
  query: { lang: 'hi' },
});
data.rooms[0].verdict;  // "avoid"
data.rooms[0].reading;  // Devanagari sentence
```

## Ship it

- **Vercel or Cloudflare Pages.** The route in Step 6 is a standard handler, deploy as is with `ROXY_API_KEY` set as a secret.
- **Static reports.** Run the four calls at build time for each listing and inline the responses into the page with Option A. No runtime calls, no key on the edge.

## Ready-made template

There is no Vastu only template yet. The flagship [astrology-ai-chatbot](https://github.com/RoxyAPI/astrology-ai-chatbot) connects every domain over Remote MCP by default, so cloning it gives you a working entrance and room checker in chat with no wiring; browse the catalog at [/templates](/templates). For a custom build, the [Next.js integration guide](/docs/integrations/nextjs) is the fastest path.

## Gotchas

- **`doorPosition` needs a cardinal facing.** An intercardinal facing names no single side, so `facing: "Northeast"` with a `doorPosition` returns 400. Send `door` as a coordinate instead, and it is snapped to the nearest edge.
- **Send one of each pair.** `door` or `doorPosition`, `facing` or `facingDegrees`, `width` plus `depth` or `polygon`, a room `direction` or a room `polygon`. Each pair returns 400 naming the pair.
- **`slopeLowDirection` is where the ground is LOW.** The verses are stated in terms of the side that stands higher, and `slope.highDirection` comes back beside it. Omit it entirely for level ground.
- **The mandala is aligned to the compass, not to the building.** `facing` never rotates the grid. A kitchen in the south-east is in the south-east whichever way the front door looks.
- **A room at the centre is judged on the brahmasthan.** Send it as a `polygon` over the middle of the plan; `zone` comes back `Center` and the remedy says to keep the centre open. A `direction` can only name one of the eight quarters.
- **`remedy` is absent on an ideal room.** Read it as optional. Rendering it unconditionally prints nothing, which is correct, and never a contradiction.
- **The 64 pada grid names no devata.** On `grid: "64-pada"` the entrance returns the pada and effect with no devata, and the mandala carries no marma, vamsa or atimarma, because the chapter gives that division structure only. Draw it with eight cells to a side.
- **`source.text` of `convention` is an answer, not a gap.** Many verdicts rest on later practice because the chapter states no rule. Render `basis` rather than hiding the row; that split is the product.
- **Every call bills at a flat 1 request.** Four calls per report, or one if you cache the catalogue and the mandala and only re-run rooms while the user edits.

## What to build next

- The [Vastu guide](/docs/guides/vastu) walks every endpoint in the domain, including Ayadi shadvarga and the two catalogues.
- The [feng shui guide](/docs/guides/feng-shui) covers the other directional system on the same key. The two share the eight sector identifiers and the facing convention, so one compass input drives both on the same plan.
- The [AI chatbot tutorial](/docs/tutorials/ai-chatbot) shows tool registration so users can ask about their floor plan in natural language.
