Add Saju compatibility to a dating, social, or matchmaking app.

Korean Saju (사주 / Four Pillars of Destiny — Bazi in Chinese) has been used for marriage and relationship matching for centuries. This tutorial wires a 0–100 compatibility score into your app with a single REST call. Send two birthdates, get back a match number and a breakdown. Free tier, clean JSON, 10 languages.

Why a compatibility score belongs in your app

Match percentages drive engagement. A profile that reads “87% compatible” gets opened, screenshotted, and shared far more than a plain list. In Korea, Saju-based gunghap (궁합) compatibility is a familiar, trusted framing for exactly this. The endpoint below turns two birthdates into a deterministic number you can surface on a profile card, a swipe screen, or a daily “best match” push.

The score is computed from real Four Pillars structure — not random — so the same two people always return the same result, which keeps your UI stable and cacheable.

1Get a free API key

Keys are self-serve. Post an email and you get a free-tier key instantly. The key is shown once — store it as a server-side secret and never ship it in client code.

curl -X POST https://saju-api.pages.dev/api/v1/keys/create \
  -H "Content-Type: application/json" \
  -d '{ "email": "dev@yourcompany.io" }'
const res = await fetch("https://saju-api.pages.dev/api/v1/keys/create", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "dev@yourcompany.io" })
});
const { api_key } = await res.json();
// store api_key server-side as a secret
import requests

r = requests.post(
    "https://saju-api.pages.dev/api/v1/keys/create",
    json={"email": "dev@yourcompany.io"},
)
api_key = r.json()["api_key"]  # keep this server-side

Free tier returns 100 requests/day at 1 request/second — enough to build, test, and run a small launch. Authenticate every call with the header X-API-Key: <your key>.

2Score two people in one call

Send each person’s solar birthdate (and birth hour if you have it). Use gender of "M" or "F", and hour as a 24-hour integer (use -1 when the birth time is unknown). The response is a 0–100 score plus a breakdown.

curl -X POST https://saju-api.pages.dev/api/v1/compatibility \
  -H "X-API-Key: sajuapi_free_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "person_a": { "year": 1990, "month": 5, "day": 15, "hour": 13, "gender": "M" },
    "person_b": { "year": 1992, "month": 9, "day": 20, "hour": 8,  "gender": "F" },
    "lang": "en"
  }'
async function compatibility(a, b) {
  const res = await fetch("https://saju-api.pages.dev/api/v1/compatibility", {
    method: "POST",
    headers: {
      "X-API-Key": process.env.SAJU_API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ person_a: a, person_b: b, lang: "en" }),
  });
  if (!res.ok) throw new Error("saju_api_" + res.status);
  return res.json();
}

const match = await compatibility(
  { year: 1990, month: 5, day: 15, hour: 13, gender: "M" },
  { year: 1992, month: 9, day: 20, hour: 8,  gender: "F" }
);
console.log(match.score); // e.g. 75
import os, requests

def compatibility(a, b):
    r = requests.post(
        "https://saju-api.pages.dev/api/v1/compatibility",
        headers={"X-API-Key": os.environ["SAJU_API_KEY"]},
        json={"person_a": a, "person_b": b, "lang": "en"},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()

match = compatibility(
    {"year": 1990, "month": 5, "day": 15, "hour": 13, "gender": "M"},
    {"year": 1992, "month": 9, "day": 20, "hour": 8,  "gender": "F"},
)
print(match["score"])  # e.g. 75

Example response

{
  "score": 75,
  "breakdown": {
    "element_balance": 25,
    "day_master_relation": 30,
    "branch_harmony": 0,
    "branch_clash": 0
  },
  "person_a": { "day_master": "경", "day_master_element": "metal", "zodiac": "말" },
  "person_b": { "day_master": "기", "day_master_element": "earth", "zodiac": "원숭이" },
  "tier": "free",
  "remaining": 98
}

What the breakdown means

The total score is the sum of four signals. You can show only the headline number, or expose the breakdown as “why you matched” copy on a profile.

FieldRangeMeaning
element_balance0–25How evenly the two charts’ Five Elements combine. Higher = a more balanced pair.
day_master_relation5–30The relationship between each person’s Day Master element — generating cycles score highest.
branch_harmony0–25Earthly Branch harmonies (six-harmony, three-harmony) between the charts.
branch_clashnegativePenalty when branches clash. A flat tension signal you can surface as “needs work.”

Wire it into a match feed

A common pattern: for a viewer, score them against each candidate, then sort the feed by score. Cache results — the score for a fixed pair never changes — so you only call the API once per unique pairing.

// pseudo-feed: rank candidates for one viewer
const ranked = [];
for (const candidate of candidates) {
  const cacheKey = pairKey(viewer.birth, candidate.birth);
  let m = cache.get(cacheKey);
  if (!m) {
    m = await compatibility(viewer.birth, candidate.birth);
    cache.set(cacheKey, m); // result is deterministic — cache forever
  }
  ranked.push({ candidate, score: m.score });
}
ranked.sort((x, y) => y.score - x.score);

Birthdates are personal data. Keep the API key on your server, call the API server-side, and store only what your privacy policy covers. The API itself is stateless — it does not retain the birthdates you send.

Localize the result

Pass lang to get localized element and zodiac labels for international users. Supported values include en, ko, ja, zh, es, pt, id, vi, th, and hi — 10 languages total, so a single integration serves a global user base.

Keep building

Need the raw Four Pillars instead of a score? See the interpret endpoint for Ten Gods, Yongshin, and luck cycles, or the quickstart for the calculate endpoint. Want a daily engagement loop? See the daily fortune & horoscope API use case.