Calculate Four Pillars from any language — no library to install.

Computing Korean Saju (사주 / Four Pillars of Destiny — Bazi in Chinese) in code usually means pulling in a lunar-calendar package, a Bazi calculator, and reconciling their solar-term tables. This API does the calendar math, true-solar-term boundaries, and chart logic on the server, so your client is a thin fetch. Copy a wrapper below and you have working Four Pillars in minutes.

Why call an API instead of installing a library

Base URL: https://saju-api.pages.dev · Auth header: X-API-Key: <your key>. Grab a free key from the quickstart (100 requests/day, no card).

Drop-in client wrapper

One small client that covers every endpoint. Keep the key in an environment variable on your server, never in client-side code.

// saju.js — works in modern browsers, Deno, and Node 18+ (built-in fetch)
const BASE = "https://saju-api.pages.dev";

function client(apiKey) {
  async function post(path, body) {
    const res = await fetch(BASE + path, {
      method: "POST",
      headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!res.ok) throw new Error("saju_api_" + res.status);
    return res.json();
  }
  async function get(path, params) {
    const qs = new URLSearchParams(params).toString();
    const res = await fetch(BASE + path + "?" + qs, {
      headers: { "X-API-Key": apiKey },
    });
    if (!res.ok) throw new Error("saju_api_" + res.status);
    return res.json();
  }
  return {
    calculate:     (b) => post("/api/v1/calculate", b),
    interpret:     (b) => post("/api/v1/interpret", b),
    compatibility: (b) => post("/api/v1/compatibility", b),
    daily:         (p) => get("/api/v1/daily", p),
  };
}

const saju = client(process.env.SAJU_API_KEY);
const chart = await saju.calculate({
  year: 1990, month: 5, day: 15, hour: 13, gender: "M", lang: "en",
});
console.log(chart.day_master); // { stem, element, polarity }
# saju.py — requires: pip install requests
import os, requests

BASE = "https://saju-api.pages.dev"

class Saju:
    def __init__(self, api_key):
        self.s = requests.Session()
        self.s.headers["X-API-Key"] = api_key

    def _post(self, path, body):
        r = self.s.post(BASE + path, json=body, timeout=10)
        r.raise_for_status()
        return r.json()

    def _get(self, path, params):
        r = self.s.get(BASE + path, params=params, timeout=10)
        r.raise_for_status()
        return r.json()

    def calculate(self, **b):     return self._post("/api/v1/calculate", b)
    def interpret(self, **b):     return self._post("/api/v1/interpret", b)
    def compatibility(self, **b): return self._post("/api/v1/compatibility", b)
    def daily(self, **p):         return self._get("/api/v1/daily", p)

saju = Saju(os.environ["SAJU_API_KEY"])
chart = saju.calculate(year=1990, month=5, day=15, hour=13, gender="M", lang="en")
print(chart["day_master"])  # {'stem': ..., 'element': 'metal', 'polarity': 'yang'}
// saju.cjs — CommonJS, Node 18+ (global fetch)
const BASE = "https://saju-api.pages.dev";

function createClient(apiKey) {
  async function post(path, body) {
    const res = await fetch(BASE + path, {
      method: "POST",
      headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!res.ok) throw new Error("saju_api_" + res.status);
    return res.json();
  }
  return {
    calculate:     (b) => post("/api/v1/calculate", b),
    interpret:     (b) => post("/api/v1/interpret", b),
    compatibility: (b) => post("/api/v1/compatibility", b),
  };
}

module.exports = { createClient };

// usage:
// const { createClient } = require("./saju.cjs");
// const saju = createClient(process.env.SAJU_API_KEY);
// const chart = await saju.calculate({ year:1990, month:5, day:15, hour:13, gender:"M" });
// saju.go
package saju

import (
    "bytes"
    "encoding/json"
    "net/http"
)

const base = "https://saju-api.pages.dev"

type Client struct{ key string }

func New(apiKey string) *Client { return &Client{key: apiKey} }

func (c *Client) post(path string, body any, out any) error {
    b, _ := json.Marshal(body)
    req, _ := http.NewRequest("POST", base+path, bytes.NewReader(b))
    req.Header.Set("X-API-Key", c.key)
    req.Header.Set("Content-Type", "application/json")
    res, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer res.Body.Close()
    return json.NewDecoder(res.Body).Decode(out)
}

func (c *Client) Calculate(body map[string]any) (map[string]any, error) {
    var out map[string]any
    err := c.post("/api/v1/calculate", body, &out)
    return out, err
}

Endpoints at a glance

EndpointMethodReturns
/api/v1/calculatePOSTFour Pillars (stems & branches), Five Elements distribution, Day Master, zodiac.
/api/v1/interpretPOSTTen Gods, 12 Life Stages, hidden stems, branch interactions, Yongshin, Daeun (luck cycles).
/api/v1/compatibilityPOSTA 0–100 compatibility score with breakdown for two charts.
/api/v1/dailyGETA daily snapshot for a Day Master on a given date.

POST endpoints take { year, month, day, hour, gender, lang }gender is "M" or "F", hour is a 24-hour integer (-1 when the birth time is unknown). The daily endpoint takes query params day_master, date (YYYY-MM-DD), and lang.

Sample calculate response

This is what one calculate call returns — everything a library would compute, already shaped as JSON:

{
  "input": { "year": 1990, "month": 5, "day": 15, "hour": 13, "gender": "M", "lang": "en" },
  "pillars": {
    "year":  { "stem": "경", "branch": "오", "stem_hanja": "庚", "branch_hanja": "午" },
    "month": { "stem": "신", "branch": "사", "stem_hanja": "辛", "branch_hanja": "巳" },
    "day":   { "stem": "경", "branch": "진", "stem_hanja": "庚", "branch_hanja": "辰" },
    "hour":  { "stem": "계", "branch": "미", "stem_hanja": "癸", "branch_hanja": "未" }
  },
  "elements": { "wood": 0, "fire": 2, "earth": 2, "metal": 3, "water": 1 },
  "day_master": { "stem": "경", "element": "metal", "polarity": "yang" },
  "zodiac": "horse",
  "tier": "free",
  "remaining": 98
}

Handling errors & limits

Errors return a JSON body with an error code and, for validation failures, a reason. Watch for 429 when you exceed the free-tier rate — back off and retry.

StatusMeaning
400Invalid input — see reason (e.g. year_out_of_range, gender_must_be_M_or_F).
401Missing or invalid API key.
429Rate limit reached for your tier. Retry after a short delay.

Supported dates run roughly 1920–2050 (solar). Always send solar (Gregorian) dates — the server resolves the lunar and solar-term details for you.

Keep building

Prefer a spec? Grab the OpenAPI file or the Postman collection and generate a client in your stack.