# Build a zodiac dating compatibility app

> Ship a two-chart compatibility UI that returns a 0 to 100 score with strengths and challenges. Time to ship: 30 minutes.

Dating and matchmaking apps are the highest-demand pattern in the astrology API market. This tutorial wires the compatibility-score feature that sits on a match card. The underlying call is [`POST /astrology/compatibility-score`](/api-reference#tag/western-astrology/POST/astrology/compatibility-score). For the full synastry report with inter-aspects, swap one URL.

## What you can build

- Match-card compatibility badges (0 to 100 score)
- Pro-tier synastry readings with inter-aspect breakdowns
- Matrimonial apps using Vedic Gun Milan (36-point Ashtakoota)
- Numerology compatibility side-features ("Life Path 5 meets Life Path 7")
- AI matchmaker chatbots that read two users and explain the fit

## Prerequisites

1. A Roxy API key from [/account](/account).
2. Two birth dates, times, and cities. You convert each city to lat/lng/timezone in Step 1.

## Install


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

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

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

## Step 1: Location lookup (mandatory)

Every chart call needs `latitude`, `longitude`, and `timezone` (IANA string preferred over decimal). Use [`GET /location/search`](/api-reference#tag/location/GET/location/search) to convert a free-text city into those three fields. Cache the result per user.


### curl
```bash
curl "https://roxyapi.com/api/v2/location/search?q=New+York" \
  -H "X-API-Key: $ROXY_API_KEY"
# returns cities[0] with latitude, longitude, timezone (IANA name)
```

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

const { data: loc } = await roxy.location.searchCities({ query: { q: 'New York' } });
const { latitude, longitude, timezone } = loc.cities[0];
// latitude: 40.7128, longitude: -74.006, timezone: "America/New_York"
```

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

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

loc = roxy.location.search_cities(q='New York')
city = loc['cities'][0]
# city['latitude'], city['longitude'], city['timezone']
```

### PHP SDK
```php
<?php

use function RoxyAPI\Sdk\createRoxy;

$roxy = createRoxy(getenv('ROXY_API_KEY'));

$loc = $roxy->location->searchCities(q: 'New York');
$city = $loc['cities'][0];
```

**Tip: IANA timezone strings (`"America/New_York"`) DST-resolve against the birth date automatically. A January 1990 New York chart gets EST, a July chart gets EDT. Decimal offsets (`-5`) also work but never DST-correct.**

## Step 2: Call the compatibility endpoint

`calculateCompatibility` takes two people, returns a 0 to 100 overall score with breakdowns. Verified via `jq -r '.paths."/astrology/compatibility-score".post.operationId' /tmp/openapi.json`.


### curl
```bash
curl -X POST https://roxyapi.com/api/v2/astrology/compatibility-score \
  -H "X-API-Key: $ROXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "person1": {"date":"1990-07-15","time":"14:30:00","latitude":40.7128,"longitude":-74.006,"timezone":"America/New_York"},
    "person2": {"date":"1992-03-22","time":"09:00:00","latitude":34.0522,"longitude":-118.2437,"timezone":"America/Los_Angeles"}
  }'
```

### TypeScript SDK
```typescript
const { data } = await roxy.astrology.calculateCompatibility({
  body: {
    person1: { date: '1990-07-15', time: '14:30:00', latitude: 40.7128, longitude: -74.006, timezone: 'America/New_York' },
    person2: { date: '1992-03-22', time: '09:00:00', latitude: 34.0522, longitude: -118.2437, timezone: 'America/Los_Angeles' },
  },
});

console.log(data.overallScore);     // 78
console.log(data.archetype.label);  // "Kindred Spirits"
console.log(data.summary);
```

### Python SDK
```python
result = roxy.astrology.calculate_compatibility(
    person1={'date': '1990-07-15', 'time': '14:30:00', 'latitude': 40.7128, 'longitude': -74.006, 'timezone': 'America/New_York'},
    person2={'date': '1992-03-22', 'time': '09:00:00', 'latitude': 34.0522, 'longitude': -118.2437, 'timezone': 'America/Los_Angeles'},
)
print(result['overallScore'])
print(result['archetype']['label'])
```

### PHP SDK
```php
$result = $roxy->astrology->calculateCompatibility(
    person1: ['date' => '1990-07-15', 'time' => '14:30:00', 'latitude' => 40.7128, 'longitude' => -74.006, 'timezone' => 'America/New_York'],
    person2: ['date' => '1992-03-22', 'time' => '09:00:00', 'latitude' => 34.0522, 'longitude' => -118.2437, 'timezone' => 'America/Los_Angeles'],
);
echo $result['overallScore'];
echo $result['archetype']['label'];
```

### MCP
```bash
claude mcp add-json --scope user roxy-astrology '{"type":"http","url":"https://roxyapi.com/mcp/astrology","headers":{"X-API-Key":"YOUR_KEY"}}'
claude mcp add-json --scope user roxy-location '{"type":"http","url":"https://roxyapi.com/mcp/location","headers":{"X-API-Key":"YOUR_KEY"}}'
```

Then in any MCP client: "calculate compatibility between someone born July 15 1990 2:30pm in New York and someone born March 22 1992 9am in Los Angeles." The agent geocodes both cities and calls the score endpoint.

Response fields exposed: `overallScore` (0-100), `archetype` (`{ label, description }`), `categories` (`{ romantic, emotional, intellectual, physical, spiritual }` sub-scores), `keyAspects[]`, `strengths[]`, `challenges[]`, `elementBalance`, `summary`, `interpretation`.

## Render the result

`<roxy-compatibility-card>` from [`@roxyapi/ui`](/docs/ui) renders the score, category breakdown, archetype, summary, strengths, challenges, and key aspects. Pass the unwrapped SDK response, the card draws itself. No hand-built markup, no `.map` over aspects, no manual score ring. Works in vanilla HTML, React, Vue, Svelte, WordPress, anything.

### Option A: server fetch, client render

Fetch with the secret key on your server, then send the response (never the key) to the card in the browser. The `mode="astrology"` attribute tells the card it is rendering a `compatibility-score` response.

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Zodiac Compatibility</title>
  <script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@0/dist/cdn/roxy-ui.js" defer></script>
</head>
<body>
  <roxy-compatibility-card id="card" mode="astrology"></roxy-compatibility-card>

  <script type="module">
    // Your backend route holds the secret key and calls the SDK (see below).
    // It returns the unwrapped compatibility-score response as JSON.
    const scoreResponse = await fetch('/api/compatibility', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        person1: { date: '1990-07-15', time: '14:30:00', latitude: 40.71427, longitude: -74.00597, timezone: 'America/New_York' },
        person2: { date: '1992-03-22', time: '09:00:00', latitude: 34.05223, longitude: -118.24368, timezone: 'America/Los_Angeles' },
      }),
    }).then((r) => r.json());

    document.getElementById('card').data = scoreResponse;
  </script>
</body>
</html>
```

Your `/api/compatibility` route calls the typed SDK and returns the unwrapped `data`. The card never sees the key.

```typescript
// app/api/compatibility/route.ts (Next.js route handler, runs server side)
import { createRoxy } from '@roxyapi/sdk';

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

export async function POST(req: Request) {
  const { person1, person2 } = await req.json();
  const { data } = await roxy.astrology.calculateCompatibility({ body: { person1, person2 } });
  return Response.json(data); // unwrapped: pass `data`, never the { data, error } envelope
}
```

**Warning: Fetch the response on your server, where the secret key lives. Never put a secret `sk_*` key in page source: it is harvestable in seconds. For client-side fetching once published, use a publishable key (`pk_live_*` / `pk_test_*`, origin-restricted, coming soon) or proxy through a backend (the [Next.js guide](/docs/integrations/nextjs) is the drop-in pattern).**

### Option B: no-build, server-rendered (inline JSON)

When the page is served from a static host or a cache and there is no client JavaScript to set `.data`, fetch on your server with the secret key and inline the unwrapped response into the card as a child `<script type="application/json" class="roxy-data">`. The component reads it on load. No key in the browser, same `<roxy-compatibility-card>` render. This is Pattern 7 from the [UI components page](/docs/ui).

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Zodiac Compatibility</title>
  <script src="https://cdn.jsdelivr.net/npm/@roxyapi/ui@0/dist/cdn/roxy-ui.js" defer></script>
</head>
<body>
  <roxy-compatibility-card mode="astrology">
    <script type="application/json" class="roxy-data">
      {
        "overallScore": 43,
        "archetype": { "label": "Growth Partners", "description": "This relationship is a catalyst for transformation." },
        "categories": { "romantic": 78, "emotional": 28, "intellectual": 58, "physical": 36, "spiritual": 15 },
        "summary": "Compatibility score: 43/100 (moderate).",
        "keyAspects": [
          { "planet1": "Uranus", "planet2": "Jupiter", "type": "TRINE", "orb": 0.07, "description": "Uranus and Jupiter connect harmoniously." }
        ]
      }
    </script>
  </roxy-compatibility-card>
</body>
</html>
```

Your server template writes the JSON in. The inlined JSON is the exact shape `calculateCompatibility` returns, the same object you would assign to `.data`. Inline the unwrapped response, never the SDK envelope. Setting the JavaScript `.data` property later always wins over the inlined JSON, so one card tag covers both server-rendered and dynamic pages with no branching.

### React

In React projects use `@roxyapi/ui-react`. The card types `mode` as a literal-union prop, so a typo fails the build. Import the response type from the SDK rather than declaring a local interface.

```tsx
'use client';

import { RoxyCompatibilityCard } from '@roxyapi/ui-react';
import type { PostAstrologyCompatibilityScoreResponse } from '@roxyapi/sdk';

export function CompatibilityView({ data }: { data: PostAstrologyCompatibilityScoreResponse }) {
  return <RoxyCompatibilityCard data={data} mode="astrology" />;
}
```

### Full synastry dual-wheel

For the inter-aspect view, swap the endpoint to [`POST /astrology/synastry`](/api-reference#tag/western-astrology/POST/astrology/synastry) (`calculateSynastry`) and the component to `<roxy-synastry-chart>`. The synastry response carries `compatibilityScore`, an `interAspects[]` table, and `analysis` with `strengths` and `challenges`. The chart renders the inter-aspects table plus the score from this response directly.

```typescript
// Server side
import { createRoxy } from '@roxyapi/sdk';

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

const { data } = await roxy.astrology.calculateSynastry({
  body: {
    person1: { date: '1990-07-15', time: '14:30:00', latitude: 40.71427, longitude: -74.00597, timezone: 'America/New_York' },
    person2: { date: '1992-03-22', time: '09:00:00', latitude: 34.05223, longitude: -118.24368, timezone: 'America/Los_Angeles' },
  },
});
// Send `data` (not the envelope) to the browser, then: synastryChart.data = data;
```

**Tip: To draw the full dual-wheel rather than just the inter-aspects table, merge each person `planets` array from [`POST /astrology/natal-chart`](/api-reference#tag/western-astrology/POST/astrology/natal-chart) (`generateNatalChart`) into `data.person1.planets` and `data.person2.planets` before assigning. Without planet positions the synastry response has none, so the chart falls back to the inter-aspects table and score. For a single birth wheel on a profile page, render `<roxy-natal-chart>` with a `generateNatalChart` response.**

## Ready-made starter

The full mobile build lives at [/starters/astrology-starter-app](/starters): React Native + Expo + TypeScript, horoscopes, natal charts, synastry, transits, all wired through `@roxyapi/sdk`. Clone:

```bash
git clone https://github.com/RoxyAPI/astrology-starter-app
cd astrology-starter-app
npm install
echo "EXPO_PUBLIC_ROXYAPI_KEY=your_key" > .env.local
npm start
```

For the conversational variant ("describe two people, get the score"), the [astrology-ai-chatbot starter](https://github.com/RoxyAPI/astrology-ai-chatbot) is the flagship Next.js + Remote MCP reference.

## Upgrade to production

1. **Move the key server-side.** Browser-embedded secret keys are for prototyping only. Put the fetch behind a Next.js route, Vercel function, or Cloudflare Worker. The [Next.js integration guide](/docs/integrations/nextjs) is the drop-in recipe.
2. **Collect cities with `<roxy-location-search>`.** Drop in the geocoding input from [`@roxyapi/ui`](/docs/ui) instead of asking users to type coordinates. It emits `roxy-location-select` with `latitude`, `longitude`, and `timezone`, which feed straight into the compatibility call. Behind the scenes it hits [`/location/search`](/api-reference#tag/location/GET/location/search) (`cities[0]`).
3. **Swap to full synastry.** [`POST /astrology/synastry`](/api-reference#tag/western-astrology/POST/astrology/synastry) (`calculateSynastry`) returns inter-aspects, strengths, challenges, and a `compatibilityScore`. Same inputs, richer output, premium tier.
4. **Matrimonial India market.** Swap to [`POST /vedic-astrology/compatibility`](/api-reference#tag/vedic-astrology/POST/vedic-astrology/compatibility) for full 36-point Ashtakoota with dosha cancellation. The [vedic guide](/docs/guides/vedic-astrology) covers it end-to-end.

## Gotchas

- **Do not ship a secret API key in browser code.** Use a publishable key (`pk_live_*`, origin-restricted, coming soon) or proxy through a backend.
- **Time must include seconds.** `14:30:00` not `14:30`. Most `<input type="time">` elements return `HH:MM`, so append `:00` before sending.
- **Both persons need full birth data.** `date`, `time`, `latitude`, `longitude`, `timezone`. Missing time degrades accuracy, no server error.
- **IANA beats decimal for DST correctness.** `"America/New_York"` resolves to EST or EDT automatically. `-5` is always EST.
- **Synastry path is `/astrology/synastry`, not `/synastry`.** Nested under `/astrology`. Easy typo.
- **Calculations verified against NASA JPL Horizons.** Score breakdowns are deterministic per pair of birth data.

## What to build next

- The [tarot app tutorial](/docs/tutorials/tarot-app) adds a second reading type for dating apps with the same fetch pattern.
- The [AI chatbot tutorial](/docs/tutorials/ai-chatbot) lets users type birth data in natural language and have an LLM call this endpoint for them.
- The [astrology guide](/docs/guides/astrology) lists every Western endpoint you can swap in.
- The [vedic guide](/docs/guides/vedic-astrology) covers Gun Milan for matrimonial builds.
