API Guide

Curious how this API stack works in a real service? Try Saju analysis and AI chat in INTA – Life is Timing, an AI Saju (Four Pillars of Destiny) app that runs on the same engine. INTA is the live reference for this API. INTA's AI voice replies (TTS) run on the same stack too — a voice API is planned for later.

Quickstart

HRUM provides AI that remembers, Saju analysis and LLM resale. Follow these three steps:

The base URLs are https://api.hrum.ai (API) and https://console.hrum.ai (console). The old addresses api.float.do, console.float.do and api.inta.do still work, so if you already integrated, you don't need to change anything.

  1. Get an API key - Sign up for the console and create a key on the API keys screen (the key is shown only once, right after you create it). The full path from sign-up to error handling is in the developer Get Started guide.
  2. Send a request - Send a POST request following the example below.
  3. Handle the response - Pull the Saju data out of the JSON response.
curl -X POST https://api.hrum.ai/api/v1/partners/v1/manseryeok \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "year": 1990,
    "month": 1,
    "day": 15,
    "hour": 14,
    "minute": 30,
    "timezone": "Asia/Seoul",
    "gender": "male"
  }'

Watch your User-Agent: The default User-Agent of Python's standard urllib (Python-urllib/3.x) trips a security rule and gets 403 (error 1010). requests, the OpenAI SDK, Node and curl work fine. If you use urllib or your own HTTP client, set a User-Agent: <app-name>/<version> header yourself. (Measured: python-requests passes · OpenAI/Python passes · node passes · Python-urllib 403)

Save money — store results and reuse them. Every successful request is billed separately. Manseryeok, Zi Wei Dou Shu, Compatibility, Astrology and Lifetime Saju results for the same birth date/time and the same options are identical every time, so store the first response in your DB (for example keyed by a hash of the request body) and use the stored value after that. For results that change by date, such as Today's Saju or Fortune Flow, include the date in the key. See Idempotency for details.

API Reference

HRUM provides AI that remembers, Saju analysis and LLM resale.

RIDM Memory API

Hosts per-end-user conversation memory and adds a memory layer to ridm/* models. Memory is for ridm/* only; OpenAI/Google models are resold as is — same model, different product. Automatic YouTube transcript injection is also supported.

No sessions — one endless session. Call with the same user and the conversation simply continues. On every call the server automatically attaches the latest historyCount conversation turns and recallCount long-term memories (summary, profile, remembered facts, ongoing concerns), so you only need to send the one new message. Two numbers control how far back it looks, and memoryCap sets the maximum number of stored messages per end user. If you'd rather manage history yourself, omit historyCount and send the earlier conversation in messages the OpenAI way.

Long conversations don't get linearly more expensive. Older assistant turns are compressed automatically — the latest 2 turns stay verbatim, and earlier ones shrink step by step from 500 to 200 characters to save input tokens (user turns stay as they are). And when the user asks about something said earlier ("you told me ~ last time"), the server searches the stored conversation and quotes it with date and speaker (included in recalledMemories) — if there is no record, it says so instead of making something up.

LLM API (resale) vs RIDM Memory API — what's the difference

Same model, different product — only the prefix differs; everything else is the same. If the response carries recalledMemories and memorySaved, it went through the RIDM path.

Feature LLM API (resale) RIDM Memory API
Model choice openai/*, google/*, anthropic/*, etc. ridm/* (add the prefix)
Response format The original model response as is (OpenAI-compatible) Original model + memory fields
user parameter Ignored Required (endUserId, keeps memory)
Conversation memory, summary and profile recall ✗ ✓ (recallCount, memoryCap)
Document processing Explicit fileIds injection only Explicit fileIds + automatic recall
YouTube link handling youtube:true supported youtube:true supported
Emotion analysis (emotion) Ignored (but if an account with the emotion feature turned off in the console sends emotion:true, it gets 403 FEATURE_DISABLED regardless of model) ✓ (includes the summary cycle)
Moderation User messages only System + user messages
systemPrompt limit Ignored — resale models ignore systemPrompt, so put it in messages as {"role":"system"} Validated at 8,000 characters (ridm only)
Extra response fields costUsd, costWon (cost) costWon, recalledMemories, memorySaved, memoryCapReached, filesRecalled, emotion, customProfile
Price Cost × exchange rate × 1.10 Resale price × 1.30 (memory, recall, emotion and automatic file recall all included; no per-feature charges)
Exception — ridm/saju-report: no memory → resale margin only, no chat (reports only)

Code example comparison

// LLM API (resale) — no memory
{
  "model": "openai/gpt-5.6-terra",
  "messages": [{ "role": "user", "content": "..." }]
}

// RIDM Memory API — with the memory engine
{
  "model": "ridm/gpt-5.6-terra",  // only the prefix differs
  "user": "user_123",              // required (keeps memory)
  "messages": [{ "role": "user", "content": "..." }]
}

// Difference in the response
// Resale: { "id": "...", "choices": [...], "usage": {...}, "costWon": 1, "costUsd": 0.00045 }
// RIDM: { "id": "...", "choices": [...], "usage": {...}, "costWon": 2, "recalledMemories": [...], "memorySaved": { "id": "turn-...", "keywords": [] } }

POST /api/v1/partners/v1/chat/completions

A conversational LLM call with memory. Calls with the same user/endUserId keep past context.

Request parameters

Parameter Type Required Description
model string O Model name. RIDM: ridm/gpt-5.6-terra, etc. / Resale: openai/gpt-5.2, google/gemini-3.5-flash, etc.
user string O End-user ID issued by the partner (endUserId — the same value keeps memory)
messages array O OpenAI-format message array [{ "role": "user", "content": "..." }, ...]
systemPrompt string System instructions (optional, up to 8,000 characters — ridm/* only). Resale models ignore systemPrompt, so put it in messages as {"role":"system"}.
youtube boolean If true, injects the transcript of a YouTube link in the last message
stream boolean Whether to stream the response (default: false). See Streaming (SSE) format below — it is not the OpenAI chunk format.
fileIds string[] IDs of uploaded documents to inject (UUID, up to 5). See Document Processing API
emotion boolean Emotional state analysis (ridm/* only, default false). See Emotional State Analysis
summaryEvery integer How often summary and emotion are refreshed (in turns, 1–20, default 5, ridm/* only)
temperature number 0–2. Not passed to reasoning-only models (models where reasoning can't be turned off)
top_p number 0–1
presence_penalty / frequency_penalty number Each -2–2
seed / stop number / string|string[] Passed upstream with the same meaning as in OpenAI
response_format object OpenAI format (with json_object, messages or systemPrompt must contain the word "JSON"). Ignored by Claude models
tools / tool_choice / parallel_tool_calls array / string|object / boolean OpenAI-format function calling. Supported on OpenAI and Google models (including their ridm/ namesakes); Claude models return 400 TOOLS_UNSUPPORTED_FOR_MODEL
n / stream_options — Accepted for SDK compatibility and ignored (always one response)
max_tokens number Maximum output tokens (1–8,192; 400 if exceeded). For gpt-5.1 and later and the 5.4, 5.5 and 5.6 families, the server turns reasoning off (reasoning_effort none), so you get as much body text as you asked for. For models where reasoning can't be turned off (gpt-5, gpt-5-mini, gpt-5-nano, gpt-6-astra, o3, o3-mini, o4-mini), the server adds a 1,500-token reasoning budget to the call, and those reasoning tokens are billed as output too.
historyCount number Number of recent conversation turns the server attaches automatically (1–50). If set, you only need to send the new message in messages. If omitted, no server history is attached and only the last 10 entries of messages are used.
recallCount number How many long-term memories (past summaries) related to the question to recall (0–20, default 3). Profile, remembered facts and ongoing concerns are always included.
memoryCap number Cap on stored messages per end user (10–1000, default 100). Applied per request, not fixed. If the stored message count exceeds the cap, this turn is still saved, but the response carries memoryCapReached: true and memorySaved: null.
candor string Candor — how direct the answer is. t / f (default). ridm/* only; ignored on resale models. See Candor (working name)

Candor (working name) — candor

Even with the same Saju and the same question, people want to hear things in different ways. candor sets how direct the answer is. It works the same way in the RIDM Memory API (ridm/*) and the three Saju reading APIs (Reading, Reading Memory, Report).

  • f (F type) — default. Changes nothing; same as not sending the field.
  • t (T type) — straight talker. States an honest judgment clearly in the first paragraph and ends with something the user can do now. It doesn't blame the person and doesn't make fatalistic claims.

Old values are still accepted: blunt → t, balanced and gentle → f (gentle is no longer a separate candor).

It's a different axis from tone (polite, casual, formal), so you can use both. Example: tone: "formal" + candor: "t" = a polite but direct answer.

systemPrompt and persona take priority. If candor conflicts with the character's speech style, restrictions or format, those win.

If a crisis signal is detected in the user's message (including when the safety check fails), candor automatically falls back to f. Only requests that sent candor as t (or blunt) get candorApplied (the value actually applied, t | f) in the response, and a fallback shows as "f". Even if you send an old value, candorApplied comes back as t/f.

Tokens used for the candor instruction are not billed. Only the user's original text is stored in the conversation history (RIDM Memory). Ignored on resale models (openai/* etc.). Values that aren't allowed (e.g. "off", "harsh") return 400.

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/chat/completions \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "ridm/gpt-5.6-terra",
    "user": "member-1024",
    "messages": [
      { "role": "user", "content": "Can we pick up where we left off about changing jobs?" }
    ]
  }'

Response example

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1629292800,
  "model": "ridm/gpt-5.6-terra",
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "안녕하세요! 이전 대화를 기억하고 있어요..."
      },
      "finish_reason": "stop",
      "index": 0
    }
  ],
  "usage": {
    "prompt_tokens": 1250,
    "completion_tokens": 320,
    "prompt_tokens_details": { "cached_tokens": 0 }
  },
  "costWon": 45,
  "recalledMemories": [...],
  "memorySaved": { "id": "turn-1756200000000", "keywords": [] },
  "youtubeUsed": ["abc123def45"]
}

usage has no total_tokens — you get prompt_tokens, completion_tokens, prompt_tokens_details.cached_tokens, and for reasoning models completion_tokens_details.reasoning_tokens (already included in completion_tokens). memorySaved is the saved-turn object or null (cap exceeded, tool-call turn), and youtubeUsed is an array of the video IDs whose transcripts were injected (not URLs).

Streaming (SSE) format

With stream: true, the response comes as named SSE events, not OpenAI chunks. So the OpenAI SDK's stream parser can't read it — parse the SSE yourself (non-streaming responses are OpenAI-compatible).

Event data
event: chunk RIDM: {"delta":"…"} (includes tool_calls fragments on tool calls) / Resale: the upstream chunk object as is
event: message_done RIDM: {finish_reason, usage, costWon, recalledMemories, memorySaved, memoryCapReached, emotion?, customProfile?, budgetExceeded?, tool_calls?} / Resale: {finish_reason, usage, costUsd, costWon, budgetExceeded?}. No id, choices or model
event: error {message, code, status} — the headers (200) have already been sent, so check status for the real error status
data: [DONE] Normal end marker (last line)
event: chunk
data: {"delta":"안녕하세요"}

event: message_done
data: {"finish_reason":"stop","usage":{"prompt_tokens":1250,"completion_tokens":320,"prompt_tokens_details":{"cached_tokens":0}},"costWon":45,"recalledMemories":[],"memorySaved":{"id":"turn-1756200000000","keywords":[]}}

data: [DONE]

Error codes

Codes this API returns in addition to the common errors. With stream: true, errors after the response has started come as event: error ({message, code, status}). Resale models follow the same table.

Code HTTP Description
MODERATION_FLAGGED400Blocked by moderation (ridm: system+user, resale: user messages)
INSUFFICIENT_BALANCE402Insufficient balance (checked by reserving the full requested output up front)
MODEL_NOT_FOUND400Model not configured
INVALID_TOOLS / TOOLS_UNSUPPORTED_FOR_MODEL400Mismatched tools fields / tool calling requested on a Claude model
FILE_NOT_FOUND400fileIds includes a file that doesn't exist (or belongs to another partner)
FEATURE_DISABLED403The youtube, files or emotion option was used on an account with that feature turned off
RATE_LIMITEDHTTP 400, body status 429Upstream rate limit, or the daily resale spending cap was exceeded (default ₩100,000/day per partner)
UPSTREAM_TIMEOUTHTTP 400, body status 504Upstream response timed out (60 seconds)
UPSTREAM_INVALID_REQUEST400The upstream model rejected the request (upstream 400/422)
INVALID_RESPONSE_FORMAT400response_format: json_object but the messages don't contain the word "json" (OpenAI family)
REPORT_ONLY_MODEL400ridm/saju-report was specified — it's only for the Saju Report API
(none)400user missing for a ridm/* model · systemPrompt over 8,000 characters · unsupported model prefix
provider_not_configured503Claude models are not connected right now

Notes

Custom Profile

ridm/* models only. Stores and manages arbitrary field-name/value pairs per end user. You can save automatically extracted field values, set values yourself, or include specific fields in this turn's prompt. Values accumulate, and existing values are kept even if the schema changes (soft-update).

Request parameters (POST /api/v1/partners/v1/chat/completions)

Parameter Type Description
extractProfileFields string[] Array of field names to extract from the conversation automatically (up to 100). During the summary cycle, the LLM sorts the user's traits, preferences and situation into these field names and stores them. Empty or null extracted values are skipped and the existing value is kept (cumulative; manually set values are not overwritten by extracted ones). No extra charge.
injectProfileFields string[] Array of field names to include in this turn's prompt. Only the fields you list are injected; nothing is injected automatically. Injected fields are billed as input tokens on every turn. Pick only the fields you need.
endUserProfile object Field-name/value object. Example: { "name": "Jane Kim", "age": "35", "interest": "parenting" }. Values you set overwrite extracted values. Up to 100 fields, 150 characters per value by default (up to 500).
profileValueMaxChars number Maximum characters per value (default 150, max 500 — larger values are clamped to 500). Applies only to values stored in that request (endUserProfile and values extracted in this summary cycle). It is not stored, so send it on every request.

Request example (RIDM chat)

{
  "model": "ridm/gpt-5.6-terra",
  "user": "enduser_123",
  "messages": [
    { "role": "user", "content": "Help me plan my kid's birthday party tomorrow" }
  ],
  "extractProfileFields": ["childAge", "interests", "budget", "constraints"],
  "injectProfileFields": ["name", "childAge", "interests"],
  "endUserProfile": {
    "name": "Jane Kim",
    "childAge": "7",
    "interests": "science, art"
  },
  "profileValueMaxChars": 200
}

Profile in the response

Responses to requests that sent profile parameters (message_done when streaming) carry a customProfile field.

"customProfile": {
  "injected": ["name", "childAge", "interests"],  // fields injected into this turn's prompt
  "stored": 4,                                     // number of fields currently stored
  "extractPending": ["budget"]                     // extraction targets that have no value yet
}

Delete API

To delete a specific field, send a DELETE request:

DELETE /api/v1/partners/v1/profile/{endUserId}/field/{key}
X-Partner-Api-Key: your-api-key

Response: { "success": true, "data": { "deleted": true } } — a missing key or an unknown endUserId still returns 200 with "deleted": false (no new end user is created). This API has no error codes of its own — only the common errors.

Limits and constraints

LLM API (resale)

Resale of original LLM models such as openai/* and google/* (no memory; original response as is). On resale model requests, user, systemPrompt, recallCount, memoryCap, summaryEvery, emotion and candor are ignored (put system instructions in messages as {"role":"system"}), and the response has no memory fields (recalledMemories etc.). Price = cost × exchange rate × 1.10.

Available models (same list as GET /api/v1/partners/v1/models)

curl https://api.hrum.ai/api/v1/partners/v1/models \
  -H "X-Partner-Api-Key: your-api-key"

GET /api/v1/partners/v1/models response — not the OpenAI models.list format. Prices are in KRW (₩) per 1M tokens, and the numbers below are examples. Claude models appear in the list only when configured on the server.

{
  "success": true,
  "data": {
    "models": [
      { "modelId": "openai/gpt-5.6-terra", "upstream": "gpt-5.6-terra", "provider": "openai", "input_cost_krw": 1234, "output_cost_krw": 9876, "category": "base" },
      { "modelId": "ridm/gpt-5.6-terra", "upstream": "gpt-5.6-terra", "provider": "openai", "input_cost_krw": 1604.2, "output_cost_krw": 12838.8, "category": "ridm" }
    ]
  }
}

This API has no error codes of its own — only the common errors. It's free.

Claude models (anthropic/*, ridm/claude-*)

  • Models (13): anthropic/claude-fable-5-1 · anthropic/claude-opus-5-5 · anthropic/claude-sonnet-5-5 · anthropic/claude-haiku-5-5 · anthropic/claude-fable-5 · anthropic/claude-opus-5 · anthropic/claude-sonnet-5 · anthropic/claude-opus-4-8 · anthropic/claude-opus-4-7 · anthropic/claude-opus-4-6 · anthropic/claude-opus-4-5 · anthropic/claude-sonnet-4-6 · anthropic/claude-haiku-4-5 (resale) / add the ridm/ prefix to the same name for the RIDM Memory API (resale price × 1.3).
  • Same request format: Send the OpenAI format and the server converts it to the Claude format. Multiple system messages are merged into one.
  • temperature and top_p ignored: Claude 5.x doesn't accept sampling values, so they have no effect if sent (no error). Control the tone with the system prompt.
  • max_tokens: Defaults to 8,192 if omitted. Claude Fable 5 and 5.1 and Opus 5 and 5.5 always have internal reasoning on, so reasoning tokens are billed as output tokens, and the call adds a 1,500-token margin to your max_tokens so the answer isn't cut off.
  • Haiku 5.5 long-context tier: If a single request's input exceeds 100,000 tokens, Anthropic's official price moves to the ×5 tier, so that request's input and output tokens are billed at ×5.
  • Supported scope: Text conversation (including streaming). Claude models don't support tool calling (tools, tool_choice, parallel_tool_calls) and return 400 TOOLS_UNSUPPORTED_FOR_MODEL — OpenAI and Google resale models do support tool calling. Image input is not supported.

Thinking (reasoning) models and max_tokens: On models that use hidden thinking/reasoning, thinking tokens count toward the output limit. The server adds a margin to the call, but if max_tokens is very small (e.g. 16), the body may be empty with finish_reason: "length", and tokens consumed upstream are billed. We recommend at least 64. Affected models: openai/gpt-5 · openai/gpt-5-mini · openai/gpt-5-nano · openai/gpt-6-astra · openai/o3 · openai/o3-mini · openai/o4-mini · anthropic/claude-opus-5 · anthropic/claude-opus-5-5 · anthropic/claude-fable-5 · anthropic/claude-fable-5-1 · google/gemini-3.1-pro (including their ridm/ namesakes).

Thinking margin and billed tokens: For models where thinking can't be turned off (openai/gpt-5 · openai/gpt-5-mini · openai/gpt-5-nano · openai/gpt-6-astra · openai/o3 · openai/o3-mini · openai/o4-mini · google/gemini-3.1-pro) and models with thinking always on (anthropic/claude-opus-5 · anthropic/claude-opus-5-5 · anthropic/claude-fable-5 · anthropic/claude-fable-5-1) (including their ridm/ namesakes), the server adds a thinking margin (up to 1,500 tokens) on top of your max_tokens to make sure you get an answer. Thinking tokens are billed at the output rate, so the output tokens actually billed can exceed max_tokens (e.g. a max_tokens 200 request billed 828 tokens: 747 thinking + 81 body). If you need a strict cost cap, use a model whose thinking turns off, such as google/gemini-3.5-flash.

POST /api/v1/partners/v1/chat/completions

Request parameters (no memory layer)

Parameter Type Required Description
model string O ridm/gpt-5.6-terra (memory) · for resale, openai/gpt-5.2, google/gemini-3.5-flash, etc. — full list on the pricing page
messages array O OpenAI-format messages
temperature number 0.0–2.0 (model default if omitted). top_p (0–1), presence_penalty and frequency_penalty (-2–2), seed, stop, response_format, and tools, tool_choice, parallel_tool_calls are accepted with the same ranges as RIDM (no tool calling on Claude)
systemPrompt string Ignored — resale models ignore systemPrompt, so put it in messages as {"role":"system"}
fileIds string[] IDs of documents to inject (UUID, up to 5, explicit injection only)
max_tokens number Maximum output tokens (1–8,192; 400 if exceeded). On thinking (reasoning) models, thinking tokens count toward this limit, so if it's too small (e.g. 16) the body may be empty with finish_reason: "length", and tokens consumed upstream are billed. We recommend at least 64.
emotion boolean Ignored (accounts with the emotion feature turned off get 403 FEATURE_DISABLED on emotion:true)
summaryEvery number Ignored

Usage example (OpenAI SDK compatible)

const client = new OpenAI({
  apiKey: 'your-float-api-key',
  baseURL: 'https://api.hrum.ai/api/v1/partners/v1',
  defaultHeaders: { 'X-Partner-Api-Key': 'your-float-api-key' }
});

const response = await client.chat.completions.create({
  model: 'openai/gpt-5.6-terra',
  messages: [{ role: 'user', content: 'Hello' }]
});
// RIDM model example: model: 'ridm/gpt-5.6-terra'

Response

Non-streaming responses use the OpenAI-compatible schema plus costUsd, costWon and requestId. stream: true is not the OpenAI chunk format, so the OpenAI SDK's stream parser can't read it — parse it yourself following Streaming (SSE) format.

{
  "id": "chatcmpl-1791452814000",
  "object": "chat.completion",
  "created": 1791452814,
  "model": "openai/gpt-5.6-terra",
  "usage": { "prompt_tokens": 9, "completion_tokens": 12, "prompt_tokens_details": { "cached_tokens": 0 } },
  "choices": [
    { "index": 0, "message": { "role": "assistant", "content": "Hello! How can I help?" }, "finish_reason": "stop" }
  ],
  "costUsd": 0.000162,
  "costWon": 1,
  "requestId": "resale-..."
}

Error codes are the same as in the RIDM Memory API error table. Resale models are also subject to the daily spending cap (RATE_LIMITED).

AI Saju Reading API

Generative AI analyzes a Saju (Four Pillars of Destiny) chart and returns a tailored interpretation and advice. Billed by tokens; returns only the interpretation.

POST /api/v1/partners/v1/reading

Generates an AI Saju reading from a Saju chart. The internal prompts and theory are not disclosed.

Request parameters

Parameter Type Required Description
speed string fast (default, quick response) | value (slower response, 20% off)
birth object Either birth or saju {year, month, day, hour?, minute?, gender, calendar?}. calendar: solar (default) | lunar — lunar dates are converted to solar before calculation. Birthplace, time zone and leap month (2026-10-17): accepts the same optional fields as the Manseryeok API — isLeapMonth, timezone, birthLatitude, birthLongitude, country, zaSiRule, birthTimePrecision — with the same names and rules, so the same birth data gives the same four pillars as Manseryeok. Send both coordinates; sending only one returns 400 INVALID_LOCATION. If you send none of them, calculation is exactly as before (birth time in Korean time, lunar dates as non-leap months). The astrology part draws the chart from the clock time actually in use at that birth moment in timezone (including daylight saving time) and the coordinates. If birth.timezone isn't recognized, the response carries warnings (field: "birth.timezone")
saju object Either birth or saju Enter the four pillars directly: {"year":{"stem":"경","branch":"오"},"month":{…},"day":{…},"hour"?:{…}}. If sent together with birth, they are cross-checked and saju wins (sajuMismatch)
year / month / day / gender number / string Flat fields (legacy). Accepted as birth (solar) only when all four are present. With no birth, saju or flat fields, you get 400 MISSING_SUBJECT_INPUT
hour / minute number Birth hour 0–23 / minute 0–59 (optional). If hour is omitted, the hour pillar is left out and the reading uses the three pillars of year, month and day
question string O The question for the reading (e.g. "What does my life path look like?")
tone string Response tone ("formal", "casual", "warm"; default: "warm")
candor string Candor — how direct the answer is. t / f (default). No charge. See Candor (working name)
persona string A character chosen by your app (name, speech style, attitude; up to 2,000 characters). Applies to speech style only and does not override the Saju interpretation, safety boundaries or length instructions. Billed as input like the question (characters × 0.25). If you offer several characters, pick from an allowlist on your server — don't pass text sent by the browser straight through
maxLength number Maximum response length (100–2,000; default: 800)
tools object Extra feature flags. Example: {"astrology": true, "youtube": true}. Unknown keys return 400 UNKNOWN_TOOL
dream string A dream the end user described (1–1,000 characters). If sent, Dream Interpretation is included in the reading automatically. See Included modules
exclude string[] Modules to leave out. ziwei (Zi Wei Dou Shu) · astro (astrology) · dream (Dream Interpretation) · tarot (Tarot). Excluded modules drop out of both the evidence and the price. Example: ["ziwei", "astro"]. The old value today is accepted but has no effect (today's fortune is no longer an included module)
tarot object If sent, Tarot cards are drawn for this reading and read along with it. Same options as the Tarot API: either count (1–10) or spread, plus reversed and seed. The card language follows the question's language. The drawn cards (including image URLs) come back in tarot in the response
only string Calls just one of the AIs inside the Saju AI on its own. saju · ziwei · astro · tarot · dream. Sending it together with exclude returns 400. See Just one AI

Request parameters — tools options

Option Type Description
astrology boolean If true, the Western astrology natal chart is factored into the reading. No separate charge
youtube boolean If true, injects the transcript of a YouTube link inside question (transcript characters × 0.25 billed as input). There is no top-level youtube field — it must be inside tools
fileIds string[] IDs of documents to inject (UUID, up to 5)

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/reading \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "question": "Is this a good year to change jobs?",
    "birth": { "year": 1990, "month": 5, "day": 15, "hour": 14, "minute": 30, "gender": "female" },
    "maxLength": 600,
    "tone": "warm"
  }'

Response example

{
  "success": true,
  "data": {
    "interpretation": "당신의 사주에서 보이는 특징은...",
    "sajuSource": "computed",
    "usage": {
      "inputTokens": 450,
      "outputTokens": 280
    },
    "costPoints": 18
  }
}

Response fields

Field Description
interpretationInterpretation text
sajuSourcecomputed (calculated from birth) | provided (used the saju input)
sajuMismatchWarning when birth and saju were both sent and the pillars differ ({pillars, computed, note}); otherwise null/omitted
toolsUsedTools that actually ran (e.g. ["youtube"])
toolsSkippedTools requested but skipped [{tool, reason}], reason: unavailable | cap_exceeded | error | auto_rejected
filesUsedInjected document IDs (when tools.fileIds is used)
usageToken counts for reference (inputTokens is the actual value including Saju and system prompts — not the billing basis)
costPointsAmount actually charged (₩)
modulesUsedModules actually included (e.g. ["saju","ziwei","astro"], saju first). Present only when the included-modules feature is on
moduleFeesFixed fee per included module (₩) (e.g. {"ziwei":50,"astro":20}). Already part of costPoints. Present only when the included-modules feature is on
tarotThe drawn cards when Tarot is included — same shape as the Tarot API response (with imageUrl in cards[], without question and costPoints)
onlyEchoes the request's only value (only if sent). If only isn't saju, Saju isn't used, so there is no sajuSource
supportCrisis support info {message, contacts:[{name, number, hours}]}. If the dream text contains crisis language, the AI is not called; interpretation carries help information and the response comes back with costPoints: 0. Also attached when the safety check couldn't be completed

Error codes

Codes this API returns in addition to the common errors:

HTTP Code
400MISSING_SUBJECT_INPUT · INVALID_SAJU_PILLAR · INVALID_LUNAR_DATE · UNKNOWN_TOOL · INVALID_FILE_IDS · FILE_IDS_TOO_MANY · INVALID_FILE_ID_TYPE · FILE_NOT_FOUND · INVALID_TOOL_MODE · MEMORY_REQUIRES_MEMORY_ENDPOINT · EMOTION_REQUIRES_MEMORY · MISSING_ENDUSER_ID (memory) · ONLY_WITH_EXCLUDE · ONLY_REQUIRES_INPUT · ONLY_DOMAIN_UNAVAILABLE · ONLY_INPUT_INVALID (Just one AI)
402INSUFFICIENT_BALANCE — before the call, the output cost of maxLength × 2 tokens (+4,000 for the value tier or for non-Korean questions) is reserved up front, so a request can be rejected even if the final charge would have been smaller
403FEATURE_DISABLED — a feature turned off for the account (youtube, files, emotion) was used

Included modules — Zi Wei Dou Shu · Astrology · Dream Interpretation · Tarot

The Saju AI contains Saju, Zi Wei Dou Shu, astrology, Tarot and Dream Interpretation AIs. The day pillar of the date (today's energy) is reflected in every reading by default, without a separate call (no extra charge). One combined call reads everything together, using the calculated results of the included modules as evidence; leave out what you don't want with exclude. To call just the one AI you need, use only. If you only need the calculated results, you can call the same modules one by one through the calculation endpoints (/ziwei-chart · /astrology (Astrology Total) · /dream · /tarot) and build your own app. /reading/memory takes the same fields.

ModuleWhen it's includedWhat goes in as evidencePrice (= one call of the standalone endpoint)
Zi Wei Dou Shu ziweiBy default when there is a birth date + birth time (hour)Chart summary — Life Palace, Body Palace, the three-and-four palace set, the four birth-year transformations, the current decade and annual periods/ziwei-chart
Astrology astroBy default when there is a birth date (time not required)Natal chart summary — signs and degrees of the Sun, Moon and Mercury through Saturn, and the Ascendant (when possible)₩20 (the standalone astrology product is /astrology)
Dream Interpretation dreamWhen you send a dream in dreamThe dream text + symbol words found in the dream/dream (KRW 0 if there are no candidates)
Tarot tarotWhen you send tarotThe cards drawn in this request and their upright/reversed meanings (returned in tarot with image URLs)/tarot

Request example (POST /api/v1/partners/v1/reading) — the Dream Interpretation dictionary is Korean-only, so the dream text stays in Korean

{
  "birth": { "year": 1990, "month": 5, "day": 15, "hour": 14, "gender": "female" },
  "question": "I can't stop thinking about last night's dream. What could it mean for me right now?",
  "dream": "큰 뱀이 집 안으로 들어와 내 발 옆에 똬리를 틀었어요",
  "tarot": { "spread": "past-present-future" }
}

Response example

{
  "success": true,
  "data": {
    "interpretation": "뱀이 집 안으로 들어와 곁에 머무는 꿈은...",
    "sajuSource": "computed",
    "modulesUsed": ["saju", "dream", "ziwei", "astro", "tarot"],
    "moduleFees": { "dream": 30, "ziwei": 50, "astro": 20, "tarot": 20 },
    "tarot": {
      "lang": "ko",
      "count": 3,
      "spread": { "type": "past-present-future", "name": "과거·현재·미래", "description": "시간 흐름을 따라가는 질문", "positions": ["과거", "현재", "미래"] },
      "reversedAllowed": true,
      "seed": null,
      "cards": [
        { "position": 1, "positionLabel": "과거", "id": "wands_10", "name": "완드의 10", "nameEn": "Ten of Wands", "arcana": "wands", "reversed": true, "keywords": ["내려놓기", "위임", "해방"], "meaning": "무거운 짐을 마침내 내려놓거나 도움을 받기 시작하는 시기.", "imageUrl": "https://console.hrum.ai/tarot/cards/v2/wands_10.webp" }
      ],
      "cardBackUrl": "https://console.hrum.ai/tarot/cards/v2/back.webp"
    },
    "usage": {
      "inputTokens": 3140,
      "outputTokens": 760
    },
    "costPoints": 162
  }
}

Saju only (leave out all other modules) (POST /api/v1/partners/v1/reading)

{
  "birth": { "year": 1990, "month": 5, "day": 15, "hour": 14, "gender": "female" },
  "question": "Is this a good year to change jobs?",
  "exclude": ["ziwei", "astro"]
}

Just one AI — only

The Saju AI contains Saju, Zi Wei Dou Shu, astrology, Tarot and Dream Interpretation AIs. Instead of one combined call as above, you can call just the AI you need with only. That AI answers from its own evidence only (chart, natal chart, drawn cards, dream candidates) and nothing else is added.

onlyWhat it callsRequired inputPrice
sajuSaju AI (Saju only, no other modules)birth or sajuToken price only (no fixed fee)
ziweiZi Wei Dou Shu AIbirth + birth time (hour)Token price + one call of /ziwei-chart
astroAstrology AIbirth (time not required)Token price + ₩20 (fixed fee for the astrology module)
tarotTarot AItarot options — no birth neededToken price + one call of /tarot
dreamDream Interpretation AIThe dream text in dream — no birth neededToken price + one call of /dream

Request example — Tarot AI only (POST /api/v1/partners/v1/reading)

{
  "question": "Should I keep pushing ahead with the job change I'm preparing?",
  "only": "tarot",
  "tarot": { "spread": "past-present-future" }
}

Response example

{
  "success": true,
  "data": {
    "interpretation": "과거 자리의 완드의 10 역방향은 오래 짊어진 짐을...",
    "only": "tarot",
    "modulesUsed": ["tarot"],
    "moduleFees": { "tarot": 20 },
    "tarot": {
      "lang": "ko",
      "count": 3,
      "spread": { "type": "past-present-future", "name": "과거·현재·미래", "description": "시간 흐름을 따라가는 질문", "positions": ["과거", "현재", "미래"] },
      "reversedAllowed": true,
      "seed": null,
      "cards": [
        { "position": 1, "positionLabel": "과거", "id": "wands_10", "name": "완드의 10", "nameEn": "Ten of Wands", "arcana": "wands", "reversed": true, "keywords": ["내려놓기", "위임", "해방"], "meaning": "무거운 짐을 마침내 내려놓거나 도움을 받기 시작하는 시기.", "imageUrl": "https://console.hrum.ai/tarot/cards/v2/wands_10.webp" }
      ],
      "cardBackUrl": "https://console.hrum.ai/tarot/cards/v2/back.webp"
    },
    "usage": {
      "inputTokens": 1180,
      "outputTokens": 720
    },
    "costPoints": 37
  }
}

Notes

AI Saju Reading · Memory API

Hosts per-end-user conversation memory and continues the reading from past context when the user comes back, for an ongoing, personalized consultation. The price is 1.3× the AI Saju Reading token rate (per 1M tokens: input KRW 3,754 · output KRW 30,030 — Reading is input KRW 2,888 · output KRW 23,100, excl. VAT).

POST /api/v1/partners/v1/reading/memory

Generates an AI Saju reading based on per-end-user memory.

Request parameters

Parameter Type Required Description
endUserId string O End-user ID issued by the partner (must be unique)
speed string fast (default, quick response) | value (slower response, 20% off)
birth object Either birth or saju {year, month, day, hour?, minute?, gender, calendar?}. calendar: solar (default) | lunar — lunar dates are converted to solar before calculation. Birthplace, time zone and leap month (2026-10-17): accepts the same optional fields as the Manseryeok API — isLeapMonth, timezone, birthLatitude, birthLongitude, country, zaSiRule, birthTimePrecision — with the same names and rules, so the same birth data gives the same four pillars as Manseryeok. Send both coordinates; sending only one returns 400 INVALID_LOCATION. If you send none of them, calculation is exactly as before (birth time in Korean time, lunar dates as non-leap months). The astrology part draws the chart from the clock time actually in use at that birth moment in timezone (including daylight saving time) and the coordinates. If birth.timezone isn't recognized, the response carries warnings (field: "birth.timezone")
saju object Either birth or saju Enter the four pillars directly: {"year":{"stem":"경","branch":"오"},"month":{…},"day":{…},"hour"?:{…}}. If sent together with birth, they are cross-checked and saju wins (sajuMismatch)
year / month / day / gender number / string Flat fields (legacy). Accepted as birth (solar) only when all four are present. With no birth, saju or flat fields, you get 400 MISSING_SUBJECT_INPUT
hour / minute number Birth hour 0–23 / minute 0–59 (optional). If hour is omitted, the hour pillar is left out and the reading uses the three pillars of year, month and day
question string O The question for the reading
tone string Response tone ("formal", "casual", "warm"; default: "warm")
candor string Candor — how direct the answer is. t / f (default). No charge. See Candor (working name)
persona string A character chosen by your app (name, speech style, attitude; up to 2,000 characters). Applies to speech style only and does not override the Saju interpretation, safety boundaries or length instructions. Billed as input like the question (characters × 0.25). If you offer several characters, pick from an allowlist on your server — don't pass text sent by the browser straight through
tagFilter string[] Memory tag filter for the search (optional)
maxLength number Maximum response length (100–2,000; default: 800)
recallCount number Number of past memories to recall (0–20, default 3)
tools object Extra feature flags. Example: {"astrology": true}. Unknown keys return 400 UNKNOWN_TOOL

Request parameters — tools options

Option Type Description
astrology boolean If true, the Western astrology natal chart is factored into the reading. No separate charge
fileIds string[] IDs of documents to inject (UUID, up to 5). If omitted, analyzed documents whose tags overlap the question are recalled automatically (filesRecalled)
emotion / summaryEvery boolean / integer Emotional state analysis (response emotion) / summary and emotion refresh interval 1–20 (default 5)
youtube boolean YouTube injection is supported only in /reading (not yet in /reading/memory) — the transcript isn't injected even if you send it

Response example

{
  "success": true,
  "data": {
    "interpretation": "당신의 사주에서 보이는 특징은...",
    "sajuSource": "computed",
    "memory": {
      "used": true,
      "summaryInjected": false,
      "notesInjected": 2,
      "tags": ["직장"],
      "overheadTokens": 120
    },
    "usage": {
      "inputTokens": 750,
      "outputTokens": 380
    },
    "costPoints": 36,
    "toolsUsed": ["memory"]
  }
}

Other response fields (sajuMismatch, toolsSkipped, filesUsed) are the same as in the AI Saju Reading API; automatically recalled documents come in filesRecalled, and with tools.emotion: true you also get emotion. The billing basis (excluding YouTube) is also the same as the Reading API.

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/reading/memory \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "endUserId": "member-1024",
    "question": "About the job change we talked about last time: is next month a good time to make the move?",
    "birth": { "year": 1990, "month": 5, "day": 15, "hour": 14, "minute": 30, "gender": "female" }
  }'

Error codes

Codes this API returns in addition to the common errors (401, 400 body validation, 402, 429, 503, etc.):

HTTPCodeWhen
400MISSING_ENDUSER_IDendUserId is empty or only whitespace
400MISSING_SUBJECT_INPUT · INVALID_SAJU_PILLAR · INVALID_LUNAR_DATEBirth data or Saju input error (same as the Reading API)
400UNKNOWN_TOOL · INVALID_TOOL_MODE · INVALID_FILE_IDS · FILE_IDS_TOO_MANY · INVALID_FILE_ID_TYPE · FILE_NOT_FOUNDtools or fileIds error. This path also accepts tools.emotion and tools.summaryEvery
400ONLY_WITH_EXCLUDE · ONLY_REQUIRES_INPUT · ONLY_DOMAIN_UNAVAILABLE · ONLY_INPUT_INVALIDErrors related to Just one AI
402INSUFFICIENT_BALANCECouldn't reserve the output cost up front (same method as the Reading API)
403FEATURE_DISABLEDA feature turned off for the account (youtube, files, emotion) was used

Memory management API

AI Saju Reading with memory (POST /reading/memory) and RIDM chat (user on ridm/* models) share the same end-user memory store. {endUserId} in the paths below is exactly the value you sent then (for example, if you split chats into sessions, the whole key, such as memberId:chatId).

Endpoint Method Description
/api/v1/partners/v1/memory/{endUserId} GET Get the end user's stored memory → {endUserId, summary, metaSummary, tags, profile, concerns, facts} (profile and facts are currently always null / [])
/api/v1/partners/v1/memory/{endUserId}/messages GET List the end user's messages. Query limit (default 50, max 200) and offset → {endUserId, messages:[{id, role, content, tags, createdAt}], offset, limit, total}
/api/v1/partners/v1/memory/search?endUserId={endUserId}&tags=… GET Search memory by tag (tags comma-separated; included in the RIDM memory price, no separate charge) → {endUserId, query:{tags}, results} (up to 20 results)
/api/v1/partners/v1/memory/{endUserId}/turns POST Stores a conversation that already happened (e.g. an AI phone consultation) as is, without AI generation. Body {messages:[{role, content}], summary?, tags?} — role is user or assistant, 1–50 messages, content up to 4,000 characters, summary up to 1,500 characters, up to 10 tags (30 characters each). Saved in the order sent, and tags are attached to every turn. If you send summary, it shows as summary in GET /memory/{endUserId}. An unknown endUserId is created. No AI call, so no separate charge → {endUserId, saved, summarySaved} (server keys only)
/api/v1/partners/v1/memory/{endUserId} DELETE Deletes all of the end user's memory, including files uploaded for that end user (/files) → 204 (no body)

Request and response examples

# Get stored memory
curl https://api.hrum.ai/api/v1/partners/v1/memory/member-1024 \
  -H "X-Partner-Api-Key: your-api-key"

{
  "success": true,
  "data": {
    "endUserId": "member-1024",
    "summary": "이직을 고민 중이며 다음 달 면접을 앞두고 있다...",
    "metaSummary": "...",
    "tags": ["이직", "면접"],
    "profile": null,
    "concerns": ["..."],
    "facts": []
  }
}
# Message list (oldest first)
curl "https://api.hrum.ai/api/v1/partners/v1/memory/member-1024/messages?limit=2&offset=0" \
  -H "X-Partner-Api-Key: your-api-key"

{
  "success": true,
  "data": {
    "endUserId": "member-1024",
    "messages": [
      { "id": "...", "role": "user", "content": "이직 고민이 있어요", "tags": ["이직"], "createdAt": "2026-10-11T09:00:00.000Z" },
      { "id": "...", "role": "assistant", "content": "...", "tags": [], "createdAt": "2026-10-11T09:00:04.000Z" }
    ],
    "offset": 0,
    "limit": 2,
    "total": 18
  }
}
# Tag search (tags as stored: 이직 = job change, 면접 = interview)
curl "https://api.hrum.ai/api/v1/partners/v1/memory/search?endUserId=member-1024&tags=이직,면접" \
  -H "X-Partner-Api-Key: your-api-key"

{
  "success": true,
  "data": {
    "endUserId": "member-1024",
    "query": { "tags": ["이직", "면접"] },
    "results": [
      { "id": "...", "summary": "다음 달 면접 일정을 이야기함", "level": 0, "tags": ["면접"], "createdAt": "2026-10-11T09:00:00.000Z" }
    ]
  }
}
# Save a conversation (no AI generation)
curl -X POST https://api.hrum.ai/api/v1/partners/v1/memory/member-1024/turns \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      { "role": "user", "content": "I'\''m worried about my interview next month" },
      { "role": "assistant", "content": "You'\''ve prepared well. You'\''ll do fine." }
    ],
    "summary": "Call about feeling anxious before next month'\''s interview",
    "tags": ["interview"]
  }'

{
  "success": true,
  "data": {
    "endUserId": "member-1024",
    "saved": 2,
    "summarySaved": true
  }
}
# Delete everything (memory + uploaded files) — 204, no body
curl -X DELETE https://api.hrum.ai/api/v1/partners/v1/memory/member-1024 \
  -H "X-Partner-Api-Key: your-api-key"

Error codes

Codes this API returns in addition to the common errors (401, 400 body validation, 402, 429, 503, etc.):

HTTPCodeWhen
404END_USER_NOT_FOUNDUnknown endUserId (common to the four paths other than saving a conversation)
400MISSING_PARAMSearch — endUserId missing
400MISSING_TAGSSearch — tags is empty
400ENDUSER_ID_TOO_LONGSaving a conversation — endUserId is longer than 255 characters

Notes

Saju Report API

One button, no question: a full Saju reading in one go, from the overview to wealth, love and the 10-year luck cycles (Daeun). A one-off paid product, separate from the conversational reading. The default is the dedicated model ridm/saju-report (RIDM's own model specialized for reports — no memory, since it's a one-shot product). The Reading API caps responses at 2,000 characters — for longer write-ups (up to 12 sections, about 6,000 characters), use this Report API.

POST /api/v1/partners/v1/reading/report

Send a birth date (or a Saju chart) and get back a Saju report in Markdown, section by section.

Request parameters

Parameter Type Required Description
birthobjectEither birth or saju{year, month, day, hour?, minute?, gender, calendar?} — calendar: "lunar" is converted to solar before calculation. Birthplace, time zone and leap month (2026-10-17): accepts the same optional fields as the Manseryeok API — isLeapMonth, timezone, birthLatitude, birthLongitude, country, zaSiRule, birthTimePrecision — with the same names and rules, so the same birth data gives the same four pillars as Manseryeok. Send both coordinates; sending only one returns 400 INVALID_LOCATION. If you send none of them, calculation is exactly as before (birth time in Korean time, lunar dates as non-leap months). The astrology part draws the chart from the clock time actually in use at that birth moment in timezone (including daylight saving time) and the coordinates. If birth.timezone isn't recognized, the response carries warnings (field: "birth.timezone")
sajuobjectEither birth or sajuEnter the four pillars directly: {"year":{"stem":"경","branch":"오"},"month":{…},"day":{…},"hour"?:{…}}
year / month / day / gendernumber / stringOptionalFlat fields (legacy, solar). Accepted as birth only when all four are present. With neither birth nor saju, you get 400 MISSING_SUBJECT_INPUT
hour / minutenumberOptionalIf hour is omitted, the hour pillar is left out and the reading uses the three pillars of year, month and day. minute is ignored if sent without hour
tonestringOptionalwarm (default) | casual | formal
candorstringOptionalCandor — t | f (default). Only the placement and strength of strengths and difficulties change; the section structure and the ban on definitive medical, legal or investment answers stay. No charge. See Candor (working name)
personastringOptionalSpeech style / character instructions (up to 2,000 characters — only this length is billed as input)
sectionsstring[]OptionalList of section titles (1–12, up to 30 characters each). The default 8: 총평 (overview) · 타고난 기질 (innate temperament) · 재물운 (wealth) · 연애·인연 (love and relationships) · 직업·적성 (career and aptitude) · 올해와 내년 (this year and next) · 10년 대운 흐름 (10-year luck cycles) · 마지막 한 줄 처방 (one-line closing advice)

Reports are always written in Korean. Fields not in the table above, such as speed, tools, lang or question, are rejected with 400.

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/reading/report \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "birth": { "year": 1990, "month": 5, "day": 15, "hour": 14, "minute": 30, "gender": "female" },
    "sections": ["총평", "재물운", "연애·인연"]
  }'

Response

{
  "success": true,
  "data": {
    "report": "## 총평\n...",        // full Markdown, section by section
    "sections": ["총평", "..."],
    "sajuSource": "computed",
    "usage": { "inputTokens": 5756, "outputTokens": 2019 },
    "costPoints": 77,
    "model": "ridm/saju-report"
    // "candorApplied": "t"  — only when candor was sent as t (t | f)
  }
}

Error codes

Codes this API returns in addition to the common errors (401, 400 body validation, 402, 429, 503, etc.):

HTTPCodeWhen
400MISSING_SUBJECT_INPUTNeither birth nor saju was sent
400INVALID_SAJU_PILLARA pillar in saju is not one of the 60 sexagenary pairs
400INVALID_LUNAR_DATEThe lunar date doesn't exist
402INSUFFICIENT_BALANCECouldn't reserve the output cost for the maximum report length (6,000 tokens) up front

YouTube Summary API

Pulls the captions of a YouTube video and returns a summary or a Q&A answer. Videos without captions return a 400 error (not billed).

POST /api/v1/partners/v1/youtube/summary

Request parameters

Parameter Type Required Description
url string O YouTube URL (e.g. https://www.youtube.com/watch?v=dQw4w9WgXcQ) or an 11-character video ID
question string A specific question (optional — if present, Q&A mode)
maxLength number Target summary length (100–4,000, default 1,000). Used as the output token budget; an exact character count is not guaranteed
tone string Tone: warm | casual | formal (default: casual). Ignored in question (Q&A) mode
lang string Caption language to fetch (default: ko). Summaries (no question) are always written in Korean, whatever the language. question (Q&A) mode can answer in the language of the question

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/youtube/summary \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "maxLength": 1000 }'

Response example

{
  "success": true,
  "data": {
    "summary": "## 0:00~1:30 오프닝\n영상 제목 설명...\n\n## 1:30~5:00 본론\n...",
    "video": {
      "videoId": "dQw4w9WgXcQ",
      "title": "Video Title",
      "channel": "Channel Name"
    },
    "transcriptSource": "supadata",
    "usage": {
      "promptTokens": 7059,
      "completionTokens": 1384
    },
    "costPoints": 51,
    "model": "ridm/gpt-5.6-terra"
  }
}

Notes

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
400INVALID_YOUTUBE_URLNo video ID found in the URL
400TRANSCRIPT_UNAVAILABLEThe video has no captions (not billed)
400LLM_GENERATION_FAILEDSummary generation failed or returned nothing
402INSUFFICIENT_BALANCECould not reserve the summary cost in advance
403FEATURE_DISABLEDThe YouTube feature is turned off for the account

Turning the feature on and off

The YouTube Summary API is opt-in per partner account and per request. Manage the account-wide setting under "Feature settings" in the console; per request, enable it with top-level youtube:true for chat and tools: { "youtube": true } for reading.

Document Processing API

Upload PDF, Word (.docx), Excel (.xlsx), PowerPoint (.pptx), CSV, text, Markdown and Hangul (.hwpx) documents, have AI analyze them, and load them into a context-aware knowledge base. Text is extracted automatically; with the "AI analysis" option on, the document is split into context sections and a summary and search tags are generated and stored for each section.

Supported formats and limits

Item Spec
Supported formats .pdf, .docx, .xlsx, .pptx, .csv, .txt, .md, .hwpx (the older Hangul .hwp format is not supported)
Max file size 10 MB
Max extracted text 200,000 characters (beyond that, charsCapped: true)
Max files per request 5 (fileIds)
Chat injection size Up to 40,000 characters per file / 80,000 in total
Analysis chunk range 3–8 sections (two passes for long documents of 40,000+ characters)
Tag range Up to 10 tags per section, up to 10 in total

Endpoints

Method Path Description
POST /api/v1/partners/v1/files Upload a file (multipart)
GET /api/v1/partners/v1/files List files (partner scope). Query endUserId, limit (default 20, max 100), offset → {files, total, limit, offset}
GET /api/v1/partners/v1/files/:id Get file details (including extracted text)
DELETE /api/v1/partners/v1/files/:id Delete a file

Uploading a file

POST /api/v1/partners/v1/files

Upload the file as a multipart form.

Request fields

Field Type Required Description
file binary O The file to upload
endUserId string End-user ID (required when analyzing)
analyze 'true'|'false' Enable AI analysis (default: 'false')

cURL example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/files \
  -H "X-Partner-Api-Key: YOUR_API_KEY" \
  -F "[email protected]" \
  -F "endUserId=user_123" \
  -F "analyze=true"

Response example

{
  "success": true,
  "data": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "name": "sample.pdf",
    "originalName": "sample.pdf",
    "mime": "application/pdf",
    "size": 125000,
    "textLength": 8942,
    "charsCapped": false,
    "textPreview": "1장. 개요 ...",
    "summary": "문서 전체 요약...",
    "context": "문서 전체 요약...",
    "status": "analyzed",
    "analysisStatus": "analyzed",
    "analyzeRequested": true,
    "endUserId": "user_123",
    "usage": { "inputTokens": 3100, "outputTokens": 420, "costWon": 1 },
    "totalChunks": 3,
    "createdAt": "2026-08-26T12:34:56Z"
  }
}

List, details, delete

# List — newest first, limit 1–100 (default 20)
curl "https://api.hrum.ai/api/v1/partners/v1/files?endUserId=user_123&limit=20&offset=0" \
  -H "X-Partner-Api-Key: YOUR_API_KEY"

{
  "success": true,
  "data": {
    "files": [ { "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "name": "sample.pdf", "status": "analyzed", ... } ],
    "total": 1,
    "limit": 20,
    "offset": 0
  }
}
# Details — upload response fields + extractedText + chunks
curl https://api.hrum.ai/api/v1/partners/v1/files/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
  -H "X-Partner-Api-Key: YOUR_API_KEY"

{
  "success": true,
  "data": {
    "id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    "name": "sample.pdf",
    ...,
    "extractedText": "1장. 개요 ...",
    "chunks": [
      { "id": "...", "chunkIndex": 0, "context": "1장 요약...", "tags": ["개요"], "textLength": 2980 }
    ]
  }
}
# Delete
curl -X DELETE https://api.hrum.ai/api/v1/partners/v1/files/f47ac10b-58cc-4372-a567-0e02b2c3d479 \
  -H "X-Partner-Api-Key: YOUR_API_KEY"

{ "success": true, "data": { "deleted": true } }

Using files in chat and readings

Explicit attachment: in Chat completions or AI reading requests, inject up to 5 documents with the fileIds parameter. Analyzed files inject the document summary (up to 500 characters) plus the original text; unanalyzed files inject the original text (original text capped at 40,000 characters per file / 80,000 in total; child chunks are not injected). Injected documents are appended to the last user message and count as input tokens. Resale models support explicit fileIds injection only.

Auto-recall (ridm/* only): on ridm/* chat and reading-memory models, analyzed files whose tags overlap the user's question are found and injected automatically (only when no explicit fileIds are given). The response lists the recalled file IDs in a filesRecalled array.

Chat request example

{
  "model": "ridm/gpt-5.6-terra",
  "messages": [
    { "role": "user", "content": "Can you summarize the main points of this document?" }
  ],
  "fileIds": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"],
  "user": "user_123"
}

Response example

{
  "id": "chatcmpl-abc123",
  "choices": [{
    "message": { "content": "문서의 주요 내용은..." },
    "finish_reason": "stop"
  }],
  "usage": { "prompt_tokens": 2500, "completion_tokens": 150 },
  "filesUsed": ["f47ac10b-58cc-4372-a567-0e02b2c3d479"]
}

filesRecalled is included only when files were auto-recalled (omitted when empty).

Pricing and billing

Turning the feature on and off

Document processing can be enabled or disabled per partner account. Manage it under "Feature settings" in the console; when it is off, every file request returns 403.

Analysis status and chunks

The GET /api/v1/partners/v1/files/:id response includes analysisStatus, extractedText and chunks. chunks is an array only when analysis is complete and totalChunks is 2 or more (each chunk: chunkIndex, context (summary), up to 10 tags); otherwise it is null. The upload response has no chunks.

Status Description
ready Extracted, not analyzed (analyze=false or not set — analyzeRequested=false)
analyzing Analysis in progress (asynchronous above 30,000 characters)
analyzed Analysis complete (includes context, tags, chunks[])
analyze_failed Analysis failed (the reason is not included in the response — upload again)

Notes

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
400FILE_NOT_PROVIDEDUpload — no file part
400INVALID_FILE_TYPEUpload — unsupported format
400FILE_TOO_LARGEUpload — over 10MB (sometimes a 413 with no code)
400END_USER_REQUIREDUpload — analyze=true but no endUserId
400(none)Details / delete — :id is not a UUID
402INSUFFICIENT_BALANCEUpload — could not reserve the analysis cost in advance
403FEATURE_DISABLEDDocument processing is turned off for the account (all four routes)
404FILE_NOT_FOUNDDetails / delete — file does not exist or belongs to another account
422EXTRACTION_FAILEDUpload — no text could be extracted from the document

Emotion Analysis (Emotion)

Analyzes the emotional state of the user and the AI by riding on the Chat API's summary cycle. There is no separate LLM call — an emotion block is only added to the summary prompt — so there is no extra charge. Supported on ridm/* models only.

Overview

Enabled when:

  1. Global: partner_emotion_enabled ✓
  2. Partner: "Emotion analysis" is turned on in the console's feature settings
  3. Request: emotion: true is set
  4. Model: ridm/* models only

Summary interval (summaryEvery)

Emotion analysis runs automatically on every summary cycle. Set the cycle interval with the summaryEvery parameter (default: 5, range: 1–20).

Request parameters

Parameter Type Required Description
emotion boolean Enable emotion analysis (ridm/* models only, default: false)
summaryEvery number Integer 1–20 (default 5). Summary and emotion update interval (in turns)

Request example

{
  "model": "ridm/gpt-5.6-terra",
  "user": "user_123",
  "messages": [
    { "role": "user", "content": "Today was a rough day..." }
  ],
  "emotion": true,
  "summaryEvery": 3
}

Response fields

Field path Type Description
emotion object|null Emotional state (null/omitted before the first cycle or when emotion=false)
emotion.user object The user's emotion
emotion.user.type10 string|null 10-class emotion label (joyful|calm|curious|concerned|excited|thoughtful|neutral|sad|angry|surprised|null)
emotion.user.tone string|null Tone (free-form string — not a fixed list, e.g. "warm") or null
emotion.user.distribution6 object 6-class distribution {joy, sadness, anger, fear, disgust, surprise}, sum = 1000 or 0
emotion.assistant object The AI's emotion
emotion.assistant.distribution6 object The AI's 6-class distribution
emotion.trend string|null Emotion trend (improving|stable|declining|null)
emotion.updatedAt ISO 8601 Time of the last emotion update
emotion.turnsSinceUpdate number Turns since the last update

Response example

{
  "id": "chatcmpl-xyz",
  "choices": [{
    "message": { "content": "오늘 하루가 힘들었군요..." },
    "finish_reason": "stop"
  }],
  "emotion": {
    "user": {
      "type10": "sad",
      "tone": "warm",
      "distribution6": {"joy": 100, "sadness": 600, "anger": 50, "fear": 100, "disgust": 50, "surprise": 0}
    },
    "assistant": {
      "distribution6": {"joy": 200, "sadness": 100, "anger": 0, "fear": 0, "disgust": 0, "surprise": 0}
    },
    "trend": "stable",
    "updatedAt": "2026-08-26T12:34:56Z",
    "turnsSinceUpdate": 0
  },
  "usage": {...}
}

Distribution fields (distribution6)

Emotion Value Description
joy 0–1000 Joy intensity
sadness 0–1000 Sadness intensity
anger 0–1000 Anger intensity
fear 0–1000 Fear intensity
disgust 0–1000 Disgust intensity
surprise 0–1000 Surprise intensity

Trend rules (trend)

Streaming response example (SSE)

In streaming mode (stream: true), the emotional state is included in the data of the event: message_done SSE event.

event: message_done
data: {"finish_reason":"stop","usage":{...},"costWon":2,"recalledMemories":[],"memorySaved":{"id":"turn-1756200000000","keywords":[]},"emotion":{"user":{"type10":"concerned","tone":"warm","distribution6":{...}},"assistant":{...},"trend":"stable","updatedAt":"2026-08-26T12:34:56Z","turnsSinceUpdate":0}}

data: [DONE]

Cost

Notes

Saju Image API

Generates AI art that expresses the Five Elements and energy of a person's Saju (Four Pillars of Destiny). Returned as a base64-encoded PNG, at a fixed charge of ₩200.

POST /api/v1/partners/v1/image

Generates a Saju Five Elements image from a date of birth.

Request parameters

Parameter Type Required Description
year number O Birth year (e.g. 1990)
month number O Birth month (1-12)
day number O Birth day (1-31)
hour number Birth hour (0-23, optional)
minute number Birth minute (0-59, optional)
gender string O Gender ("male" or "female")
style string Style ("background", "icon", "portrait", default: "background") — changes only the mood; the size stays the same

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/image \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "year": 1990, "month": 5, "day": 15, "hour": 14, "minute": 30, "gender": "female", "style": "portrait" }'

Response example

{
  "success": true,
  "data": {
    "imageDataUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
    "costPoints": 200
  }
}

Notes

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
402INSUFFICIENT_BALANCEBalance is less than the price of one image
504IMAGE_UPSTREAM_TIMEOUTNo image received within 80 seconds (not billed)

Image Generation API (model resale)

POST /api/v1/partners/v1/images/generations

Call GPT-Image and Nano Banana models with one key, in the same format as the OpenAI Images API. Each model, resolution and quality has a fixed per-image price, and the amount in the table is deducted as is (images generated × per-image price). The cost of prompt input tokens is included in the per-image price and is not added separately.

Request parameters

ParameterTypeRequiredDescription
modelstringOopenai/gpt-image-2, openai/gpt-image-1-mini, google/nano-banana-pro, google/nano-banana, fal/nano-banana-pro, fal/nano-banana
promptstringODescription of the image to generate (up to 1,000 characters). Longer → 422 PROMPT_TOO_LONG (checked after the balance reservation, so with an insufficient balance 402 comes first); an empty string → 400 EMPTY_PROMPT
sizestringGPT-Image: 1024x1024 (default) · 1024x1536 (portrait) · 1536x1024 (landscape) · auto (the model chooses — billed at the most expensive size for that quality). Legacy values 256x256 and 512x512 are generated and billed as 1024x1024, 1792x1024 as 1536x1024, and 1024x1792 as 1024x1536. On Nano Banana it is used only as a ratio — aspect_ratio is recommended.
qualitystringGPT-Image only: low · medium (default) · high. Legacy values standard=medium and hd=high still work. Sending low/medium/high to Nano Banana returns 400.
aspect_ratiostringNano Banana only: 21:9 · 16:9 · 3:2 · 4:3 · 5:4 · 1:1 (default) · 4:5 · 3:4 · 2:3 · 9:16. The ratio does not affect the price. Takes precedence over size.
resolutionstringNano Banana only: Nano Banana supports 1K only; Nano Banana Pro supports 1K (default) · 2K · 4K. Uppercase K only.
nnumberNumber of images, 1–4 (default 1). Google direct models generate only one image per call and bill only one.

Sending aspect_ratio or resolution to GPT-Image, or requesting a resolution the model does not support, is rejected with 400 INVALID_PARAMETER and not billed.

Other errors: MODEL_NOT_FOUND / INVALID_MODEL (400, unknown model), INVALID_PROMPT (400, the upstream rejected the prompt), UPSTREAM_ERROR, RATE_LIMITED (currently HTTP 400, body status 429), INSUFFICIENT_BALANCE (402).

Price per image (₩, excl. VAT)

Loading prices…

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/images/generations \
  -H "X-Partner-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/nano-banana-pro",
    "prompt": "A serene mountain landscape at sunset",
    "aspect_ratio": "16:9",
    "resolution": "2K"
  }'

Response example

{
  "created": 1791452814,
  "data": [ { "b64_json": "iVBORw0KGgo..." } ],
  "costWon": 225,
  "requestId": "..."
}

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
400MODEL_NOT_FOUNDModel is not in the price list
400INVALID_MODELPrefix other than openai/, google/ or fal/
400INVALID_PARAMETERAn option the model does not accept (aspect_ratio or resolution on GPT-Image, quality on Nano Banana, an unsupported resolution)
400EMPTY_PROMPTThe prompt is only whitespace
400INVALID_PROMPTThe upstream model rejected the prompt
400UPSTREAM_ERRORUpstream error or zero images (not billed)
HTTP 400, body status 429RATE_LIMITEDUpstream rate limit — try again shortly
402INSUFFICIENT_BALANCEPer-image price × n exceeds the balance
422PROMPT_TOO_LONGPrompt over 1,000 characters

Error responses use the same {"success": false, ...} format as other APIs (only success responses are unwrapped).

Long-running generation — asynchronous response (202, optional)

High-resolution or multi-image requests can take over a minute. If you add the Prefer: respond-async request header and the result is not ready within 60 seconds, the connection is not held open: you get 202 Accepted with a job ID, and generation continues on the server for up to 240 seconds. If it finishes within 60 seconds, you get the same 200 response as today. Without the header, nothing changes (if it does not finish within 80 seconds, 504 IMAGE_UPSTREAM_TIMEOUT, not billed) — send the header only from clients that are ready to handle 202.

curl -X POST https://api.hrum.ai/api/v1/partners/v1/images/generations \
  -H "X-Partner-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: respond-async" \
  -d '{"model": "google/nano-banana-pro", "prompt": "...", "resolution": "4K"}'

HTTP/1.1 202 Accepted
Location: /api/v1/partners/v1/jobs/imgjob_3f2a...
Retry-After: 5
Preference-Applied: respond-async

{
  "id": "imgjob_3f2a...",
  "object": "image.job",
  "status": "pending",
  "pollUrl": "/api/v1/partners/v1/jobs/imgjob_3f2a...",
  "retryAfter": 5,
  "createdAt": "2026-10-12T03:00:00.000Z",
  "expiresAt": "2026-10-13T03:00:00.000Z"
}

GET /api/v1/partners/v1/jobs/{id}

Poll with the same API key (X-Partner-Api-Key) every Retry-After seconds. Polling is free and does not count toward the daily request limit. It is limited to once every 2 seconds per API key; polling more often returns 429 IMAGE_JOB_POLL_RATE_LIMITED with Retry-After (seconds). Responses are HTTP 200; the state is given by the body's status.

statusBody
pendingStill generating — poll again after the Retry-After header / retryAfter
succeededresult = exactly the 200 response a synchronous call would have returned (data, costWon, requestId)
failederror = exactly the error body a synchronous call would have returned (statusCode, code, message) — not billed
HTTPCodeWhen
202—A Prefer: respond-async request did not finish within 60 seconds — poll with the job ID above
404IMAGE_JOB_NOT_FOUNDUnknown, expired or another partner's job ID
429IMAGE_JOB_POLL_RATE_LIMITEDPolled again within 2 seconds with the same API key — wait Retry-After seconds
error.statusCode 504IMAGE_UPSTREAM_TIMEOUTNo image received within 240 seconds (not billed)
error.statusCode 413IMAGE_JOB_RESULT_TOO_LARGEResult exceeds 16MB (not billed)
error.statusCode 503IMAGE_JOB_INTERRUPTEDJob interrupted by a server restart (not billed) — send the request again
error.statusCode 402—Insufficient balance when the result came in (not billed)

Manseryeok API

POST /api/v1/partners/v1/manseryeok

Includes Saju Identity — the identity field at the end of the response also carries Samjae (three-year misfortune cycle), Sinsal (symbolic stars), Gongmang (void), root strength, seasonal strength, the 10-year / annual luck timeline and a Saju MBTI (no extra charge; null if the calculation fails).

Calculates the Manseryeok (the Korean Four Pillars calendar) natal chart: for the four pillars (year, month, day, hour), the Heavenly Stems and Earthly Branches, Chinese characters, Five Elements, Ten Gods, hidden stems, Nayin, Twelve Life Stages and Sinsal. Using precise solar-term-based calculation, it delivers a complete Saju reading — natal chart analysis (Yongsin (favorable element), Eokbu balancing, Johu (climate) Yongsin, Day Master strength, purity/turbidity, mediating element, illness-and-remedy, Five Elements variables) → ten 10-year luck cycles (Daeun) (including the current cycle, with stem/branch combinations, clashes, punishments and harms) → five years of annual luck (Seun) (grade, relationship and interpretation per year) → Gyeokguk (chart structure) and detailed interpretation → the ten Heavenly Stems theory, seasonal state, Gongmang, phantom characters (heoja), hidden combinations (bihap) and hidden prosperity (amnok) — in 30 top-level base fields plus optional fields (200+ including sub-fields). If the birth time is unknown, only the hour pillar (wonkuk.hour) and trueSolarTime, which needs the actual time, are null; analysis fields such as Yongsin, Gyeokguk and Day Master strength are still returned, calculated from the three pillars of year, month and day (six characters) (birthHour.basis: "three_pillars").

Request parameters

Parameter Type Required Description
year number O Birth year (e.g. 1990)
month number O Birth month (1-12)
day number O Birth day (1-31)
hour number Birth hour (0-23). If omitted, only the hour pillar is left out of the response (wonkuk.hour: null); analysis fields such as Yongsin, Gyeokguk, Day Master strength and annual luck are calculated from the three pillars of year, month and day (six characters). minute is ignored if sent without hour.
minute number Birth minute (0-59, default: 0)
timezone string O Time zone (e.g. Asia/Seoul). Required — omitting it returns 400. Used to set the standard meridian when you send coordinates. With astrology: true, with or without coordinates, the birth time for natalChart is read as the clock time actually in use in this zone at that moment (including daylight saving time). An unrecognized name is calculated in the default zone and the response gets a warning (UNKNOWN_TIMEZONE)
gender string O Gender ("male" or "female", required)
name string Full name (optional)
astrology boolean Optional If true, the response includes natalChart (signs of the Sun, Moon and Mercury through Saturn). The ascendant appears only when you also send the birth time and location (birthLatitude/birthLongitude or country); without a location, signs are calculated for noon — for details such as the ascendant's estimated range, use the Natal Chart API
glossary boolean Optional If true, explanations (title, short, body, in the lang language) of the terms that appear in this chart (stem/branch characters, Ten Gods, Twelve Life Stages, Nayin, Sinsal, hidden stems, clashes and punishments, branch relations, Gyeokguk, Gongmang, Samjae) are included in the response's glossary. No extra charge. Terms with no explanation in the requested language come in Korean, with "fallbackLanguage": "ko" on that entry. glossary keys are Korean/Chinese-character codes regardless of lang
lang string Language of interpretation strings: ko (default, not translated) · en · ja · th. Uses the same translation table as the Manseryeok screen in the INTA app
calendar · isLeapMonth string · boolean With "lunar", year, month and day are read as a lunar date and converted to solar for calculation (default "solar"). isLeapMonth: true means a leap month — if that year has no such leap month, 400 INVALID_LUNAR_DATE (since 2026-10-12, not billed). The lunar calendar follows the Korea Astronomy and Space Science Institute (KASI) and accepts lunar dates from 1900-01-01 to 2050-11-30
birthLatitude · birthLongitude number Birthplace coordinates (send both). Longitude is used for the true solar time correction. The standard meridian is taken from the standard time of timezone (excluding daylight saving time). natalChart (signs) is drawn from the clock time actually in use at birth (including daylight saving time), not standard time
country string Two-letter country code of birth. Sending KR, JP or TH without coordinates corrects with that country's representative longitude (no correction for other countries)
zaSiRule string Day pillar for the Ja hour (after 23:00): "traditional" (default, next day's day pillar) · "yajaSi" (night Ja hour: same day's day pillar; after location correction, the hour-pillar stem in the 23:00 hour is based on the next day's Day Master, since 2026-10-20)
birthTimePrecision string exact · approximate · estimated · unknown. Location correction applies only with exact. If omitted and you send coordinates or country, it is treated as exact. Birth times during Korean daylight saving time (1948–51, 1955–60, 1987–88) are read as standard time minus one hour regardless of this value or location (2026-10-16, same as the INTA app). If country is not KR or timezone is not Seoul, Korean daylight saving time is not subtracted

Response fields (30 base + optional fields)

Field Type Description
wonkuk object Natal chart (the four pillars). year, month, day, hour (hour is null when the birth time is unknown). For each entry see the PillarFull structure
trueSolarTime object|null True solar time (longitude) correction result. Has a value only when you send coordinates (birthLatitude/birthLongitude) or country (KR, JP, TH) and birthTimePrecision is exact (exact is the default when a location is sent); otherwise null. Fields: original/corrected (ISO 8601), originalLocal/correctedLocal (HH:MM), longitudeCorrection/equationOfTime/totalCorrectionMinutes (minutes), dayShift, dstApplied, basis ('coords' or 'country'). null when the birth time is unknown
ohaengBalance object Five Elements balance. { 목: int, 화: int, 토: int, 금: int, 수: int } (Wood, Fire, Earth, Metal, Water)
yongshin string Final Yongsin (favorable element) — the result of the five methods (dominance, climate, balancing, mediation, illness-and-remedy); since 2026-10-08 it equals yongshinDetail.primary (e.g. "수" = Water)
johuYongshin string|null Climate (Johu) Yongsin (distinct from the balancing Yongsin)
eokbuYongshin string Balancing (Eokbu) Yongsin (chosen by weighing a strong vs. weak Day Master). May differ from the final Yongsin (yongshin)
yongshinDetail object Full Yongsin engine result. Decided by the priority of the five methods (dominance → climate → balancing → mediation → illness-and-remedy); includes method (method adopted), primary (Yongsin), secondary (supporting Yongsin), reasoning (array of rationale sentences), shin (five grades: Yongsin, Huisin, Gisin, Gusin, Hansin), favorable_sipsin/unfavorable_sipsin (favorable / unfavorable Ten Gods), and element_states (count, excess/deficiency and favorability per element). yongshin above equals this engine's primary (since 2026-10-08). Use this field for interpretations that need the rationale. Also returned when the birth time is unknown, calculated from the three pillars of year, month and day (six characters).
ilganSummary string Day Master (ten Heavenly Stems theory). E.g. "壬水 — 큰 바다의 기운. 지혜롭고 포용력이 넓어요." (uses only the Day Master, so it is returned whether or not the birth time is known)
daeun array Ten 10-year luck cycles (always 10). Each entry has the starting age, Heavenly Stem and Earthly Branch, Five Elements, Ten Gods, Sinsal and isCurrent (whether it is the current cycle). A cycle runs from startAge inclusive to endAge exclusive, so one cycle's endAge equals the next cycle's startAge. Empty array if gender is missing or "other". See the DaeunPeriod structure
seun array Five years of annual luck — two years either side of this year (this year −2 to +2, ascending), with isCurrent: true only on this year's entry. Each entry has the year, Heavenly Stem and Earthly Branch, Five Elements, grade (good/caution/excellent/neutral), relationships (harm, six combination, punishment, etc.) and a summary. See the SeunYear structure
branchRelations array Earthly Branch relations. List of branch pairs among year, month, day and hour that have a clash, punishment or combination. Each entry has type ('沖(충)'/'刑(형)'/'半合(반합)', etc.), pillars and description (e.g. "丑未沖 (축미충)")
gongmang array The two Gongmang (void) branches — the void of the day pillar's decade (always two, whether or not those branches appear in the natal chart). E.g. ["술", "해"]
cheonEulGwiIn array Natal-chart branches that are a Cheon-eul Gwiin (Heavenly Noble). Same rule as the branches of pillars whose minorSinsal contains "천을귀인". [] if no pillar qualifies. E.g. ["묘"]
wolryeong string Month command (the governing branch of the current month, by solar-term entry). E.g. "축"
daeunNumber number Daeun number — the age at which the first 10-year cycle starts (daeun[0].startAge, e.g. 3). It is not the index of the current cycle — find the current cycle with daeun[].isCurrent
daeunDirection string Direction of the 10-year cycles. "순행" (forward) or "역행" (backward)
gyeokguk object|null Gyeokguk (chart structure). name ('정관격', etc.) and summary ('질서·명예·합리'). Also calculated from the three pillars of year, month and day (six characters) when the birth time is unknown
gyeokgukDetail object|null Detailed Gyeokguk interpretation. primary, source, trigger, interpretation (core/career/personality/challenge). Also calculated from the three pillars when the birth time is unknown
cheonganDetail object|null Ten Heavenly Stems theory in detail. ilgan, hanja, archetype, characteristics, seasonal, relations. Also calculated from the three pillars when the birth time is unknown
hapChungReport object|null Combined report on combinations, clashes and punishments of the Heavenly Stems and Earthly Branches: whether combinations/clashes in the natal chart hold, and new relations formed by the 10-year and annual luck. Also calculated from the three pillars when the birth time is unknown
heojaReport object|null Combined phantom-character (heoja) report. hapHeo, jaHeo, bihap, bihapEmergence, heojaGyeok, amrok. Also calculated from the three pillars when the birth time is unknown, except that heojaGyeok (heoja structure) is empty because it needs an actual hour branch
gongmangDetail object|null Detailed Gongmang report. Void by year pillar and day pillar, whether a Five Element is void, etc.
ohaengByeonsuHits array Automatically extracted Five Elements variables: examples where the dominance, control and smoothing formulas apply. Each entry has key, subject (element), target, formula_idiom, description, category
strength object|null Day Master strength score and grade. dayMaster, score (-100 to +100), result ('신강' strong / '신약' weak), level, breakdown (season, support, drain/control, hidden stems, rooting). Also calculated from the three pillars when the birth time is unknown
jeongtak object|null Purity/turbidity verdict. verdict ('정' pure / '탁' turbid), reasons, severity. Also calculated from the three pillars when the birth time is unknown
tonggwan object|null Whether a mediating element (tonggwan) applies. Result of compound logic
byeongyak object|null Whether illness-and-remedy (byeongyak) applies. Result of compound logic
natalChart object|null Western astrology natal chart (optional, only with astrology=true). Signs of seven bodies — the Sun, Moon and Mercury through Saturn — and the ascendant (when a location is sent)
countryCodeUsed string|null The country code used to calculate this response (when country was sent), otherwise null
identity object|null Always included (since 2026-10-10). Saju Identity — rooting_hints (rooting), season_strength_hint (seasonal strength), sinsal_list (Sinsal), gongmang (Gongmang), samjae (Samjae), timeline (10-year / annual luck timeline), mbti_inference (Saju MBTI), coherence. null if the calculation fails. See Saju Identity
ohaengDeficient array of string Always included (since 2026-10-10). Elements whose count in ohaengBalance is 0 ([] if none). If lang is not ko, translated names are returned (the keys of ohaengBalance stay in Korean)
glossary object Optional, only with glossary=true. Term key → { title, short, body, fallbackLanguage? }
lunarInput object Optional, only with calendar: "lunar". Details of the lunar → solar conversion
birthHour object Optional, only when hour is omitted. Unknown-birth-time metadata (see Notes below)

PillarFull structure (sub-fields of each pillar)

Fields of each of wonkuk.year, wonkuk.month, wonkuk.day and wonkuk.hour in the response:

Field Type Description
stem string Heavenly Stem. "갑", "을", "병", "정", "무", "기", "경", "신", "임", "계"
branch string Earthly Branch. "자", "축", "인", "묘", "진", "사", "오", "미", "신", "유", "술", "해"
stemHanja string Heavenly Stem in Chinese characters. "甲", "乙", "丙", "丁", "戊", "己", "庚", "辛", "壬", "癸"
branchHanja string Earthly Branch in Chinese characters. "子", "丑", "寅", "卯", "辰", "巳", "午", "未", "申", "酉", "戌", "亥"
ohaeng string Five Elements. One of "목", "화", "토", "금", "수" (Wood, Fire, Earth, Metal, Water)
stemSipsin string Ten Gods of the stem (relative to the Day Master). One of "정관", "편관", "정재", "편재", "식신", "상관", "정인", "편인", "비견", "겁재"
branchSipsin string Ten Gods of the branch (relative to the Day Master). The same ten values as for the stem
jijanggan array of string Hidden stems (Heavenly Stems hidden inside the branch). E.g. ["갑", "을", "목"] or ["갑", "을"]
chunghyung array of string Clashes and punishments (relations with other pillars). "沖(충)", "刑(형)", etc. E.g. ["沖(충)", "刑(형)"]
hapPosition array of string Combination positions (relations with other pillars). "半合(반합)", "육합", etc. E.g. ["半合(반합)"]
ungSung string Twelve Life Stages. One of "장생", "목욕", "관대", "건록", "제왕", "쇠", "병", "사", "묘", "절", "태", "양"
ungSungSub string (optional) Supplementary Twelve Life Stages info (optional field)
napEum string Nayin element. E.g. "성두토", "간하수", "검봉금"
majorSinsal array of string Major Sinsal. E.g. ["장성살"], ["지살"]
minorSinsal array of string Minor Sinsal. E.g. ["천을귀인", "귀문관살", "원진살", "현침살", "육해살"]

DaeunPeriod structure (each 10-year luck cycle)

Field Type Description
startAge number Starting age (the cycle begins at this age)
endAge number Ending age — this age is not included. Equals the next cycle's startAge (e.g. 3–13, 13–23)
stem string Heavenly Stem
branch string Earthly Branch
stemHanja string Heavenly Stem in Chinese characters
branchHanja string Earthly Branch in Chinese characters
ohaeng string Five Elements
isCurrent boolean Whether this is the current cycle (true = in progress now)
stemSipsin string Ten Gods of the stem
branchSipsin string Ten Gods of the branch
sinsal array of string Array of Sinsal

SeunYear structure (each year of annual luck)

Field Type Description
year number Gregorian year
stem string Heavenly Stem
branch string Earthly Branch
stemHanja string Heavenly Stem in Chinese characters
branchHanja string Earthly Branch in Chinese characters
ohaeng string Five Elements
isCurrent boolean Whether this is the current year
grade string Grade. "good" (smooth), "excellent" (very good), "caution" (be careful), "neutral" (no change)
relations array Stem/branch relations. Each entry has type (harm, six combination, punishment, etc.), target (year pillar, month pillar, etc.) and description
summary string Summary of the year's reading. E.g. "전체적으로 순조로운 흐름이 예상돼요."
sinsal array of string Array of Sinsal

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/manseryeok \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "year": 2000, "month": 1, "day": 15, "hour": 14, "minute": 30,
    "gender": "male", "calendar": "solar", "timezone": "Asia/Seoul"
  }'

Response example (excerpt: male, 2000-01-15 14:30)

{
  "success": true,
  "data": {
    "wonkuk": {
      "year": {
        "stem": "기", "branch": "묘", "stemHanja": "己", "branchHanja": "卯",
        "ohaeng": "토", "stemSipsin": "정관", "branchSipsin": "상관",
        "jijanggan": ["갑", "을"], "chunghyung": [], "hapPosition": ["半合(반합)"],
        "ungSung": "사", "napEum": "성두토",
        "majorSinsal": ["장성살"],
        "minorSinsal": ["천을귀인", "귀문관살", "원진살", "현침살", "육해살"]
      },
      "month": { ... },
      "day": { ... },
      "hour": { ... }
    },
    "trueSolarTime": null,
    "ohaengBalance": { "목": 1, "화": 2, "토": 3, "금": 1, "수": 1 },
    "yongshin": "수",
    "johuYongshin": "화",
    "yongshinDetail": {
      "method": "억부",
      "primary": "수",
      "secondary": ["금"],
      "reasoning": ["일간 임(수) 신약", "신약 + root 있음(8) → 비겁(수)"],
      "shin": { "yongshin": "수", "huishin": "금", "gishin": "토", "gushin": "화", "hanshin": "목" },
      "favorable_sipsin": ["편인", "정인", "비견", "겁재"],
      "unfavorable_sipsin": ["편관", "정관", "편재", "정재", "식신", "상관"],
      "element_states": { "목": { "count": 1, "state": "lack", "effect": "neutral" }, "...": "5오행" }
    },
    "eokbuYongshin": "수",
    "ilganSummary": "壬水 — 큰 바다의 기운. 지혜롭고 포용력이 넓어요.",
    "daeun": [
      {
        "startAge": 3, "endAge": 13,
        "stem": "병", "branch": "자", "stemHanja": "丙", "branchHanja": "子",
        "ohaeng": "화", "isCurrent": false,
        "stemSipsin": "편재", "branchSipsin": "겁재",
        "sinsal": ["년살", "장성살"]
      },
      {
        "startAge": 13, "endAge": 23,
        "stem": "을", "branch": "해", "stemHanja": "乙", "branchHanja": "亥",
        "ohaeng": "목", "isCurrent": false,
        "stemSipsin": "상관", "branchSipsin": "비견",
        "sinsal": ["지살", "망신살"]
      },
      {
        "startAge": 23, "endAge": 33,
        "stem": "갑", "branch": "술", "stemHanja": "甲", "branchHanja": "戌",
        "ohaeng": "목", "isCurrent": true,
        "stemSipsin": "식신", "branchSipsin": "편관",
        "sinsal": ["천살", "월살"]
      }
      // … cycles 4–10 (startAge 33–93) omitted — the actual response has 10
    ],
    "seun": [
      // 2024 and 2025 entries omitted
      {
        "year": 2026, "stem": "병", "branch": "오", "stemHanja": "丙", "branchHanja": "午",
        "ohaeng": "화", "isCurrent": true, "grade": "good",
        "relations": [
          { "type": "해", "target": "월주", "description": "오-축 해" },
          { "type": "육합", "target": "시주", "description": "오-미 육합" }
        ],
        "summary": "전체적으로 순조로운 흐름이 예상돼요. (해, 육합 작용)",
        "sinsal": ["육해살", "재살"]
      }
      // 2027 and 2028 entries omitted — the actual response has 5: two years either side of this year
    ],
    "branchRelations": [
      { "type": "沖(충)", "pillars": ["축", "미"], "description": "丑未沖 (축미충)" },
      { "type": "半合(반합)", "pillars": ["묘", "미"], "description": "卯未合 (묘미합)" }
    ],
    "gongmang": ["술", "해"],
    "cheonEulGwiIn": ["묘"],
    "wolryeong": "축",
    "daeunNumber": 3,
    "daeunDirection": "역행",
    "gyeokguk": {
      "name": "정관격",
      "summary": "질서·명예·합리"
    },
    "gyeokgukDetail": { ... },
    "cheonganDetail": { ... },
    "hapChungReport": { ... },
    "heojaReport": { ... },
    "gongmangDetail": { ... },
    "ohaengByeonsuHits": [
      {
        "key": "to_wang_deuk_mok",
        "subject": "土", "target": "木",
        "formula_idiom": "방능소통(方能疏通)",
        "description": "토가 왕성한데 목을 얻으면 소통이 된다",
        "category": "wang_geuk_sunwha"
      },
      {
        "key": "gang_to_deuk_geum",
        "subject": "土", "target": "金",
        "formula_idiom": "방제기해(方制其害)",
        "description": "강한 토가 금을 얻으면 그 피해를 막는다",
        "category": "gang_seolgi_sunwha"
      }
    ],
    "strength": {
      "dayMaster": "임",
      "score": -66,
      "result": "신약",
      "level": "중약",
      "breakdown": { "season": -30, "support": 8, "control": -38, "hidden": -9, "root": 8 },
      "root": { "rooted": true, "rootScore": 8 }
    },
    "jeongtak": {
      "verdict": "탁",
      "reasons": ["파괴 십신 2개 감지", "용신 금 생 ✓", "일주 신약"],
      "severity": "극탁"
    },
    "tonggwan": { ... },
    "byeongyak": { ... },
    "countryCodeUsed": null,
    "identity": {
      "rooting_hints": { ... }, "season_strength_hint": { ... }, "sinsal_list": [ ... ],
      "gongmang": { ... }, "samjae": { ... }, "timeline": { ... },
      "mbti_inference": { ... }, "coherence": { ... }
    },
    "ohaengDeficient": []
  }
}

This example was called in 2026, so daeun[].isCurrent and the years in seun depend on the year of the call. { ... } and // lines mark omissions. The request sent no location, so trueSolarTime is null. Add "country": "KR" (or coordinates) to the same request and trueSolarTime holds the correction result ("basis": "country") and countryCodeUsed becomes "KR". For the shape of the correction result, see the lunar input example in the Notes below.

Notes

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
400INVALID_LUNAR_DATE · INVALID_LOCATIONNon-existent lunar date / only one of latitude and longitude sent (not billed)
402(none)Balance is less than the call price — this API has no pre-call balance check, so the 402 comes without a code

Lifetime Saju API

POST /api/v1/partners/v1/lifetime

Calculates the natal chart from the date and time of birth and returns the reading paragraphs for the parts that apply to the chart (day pillar, Day Master × month branch, Day Master × strength, Day Master × Yongsin, up to three excess/deficient Five Elements, and the strongest Ten Gods group), grouped into seven topics: personality, wealth, career, love, health, relationships and advice. The paragraphs are written with HRUM's Saju theory, and the same input always gives the same result. The natal chart is determined exactly as in the Manseryeok API.

All seven topics
One topic (topic)
Western astrology and Zi Wei Dou Shu layers (layers)

Request parameters

ParameterTypeRequiredDescription
year · month · daynumberOBirth year (1900-2100), month and day (solar by default; lunar with calendar: "lunar")
hour · minutenumberBirth time. If hour is omitted, calculated with the same unknown-birth-time rule as the Manseryeok API (three pillars of year, month and day)
timezonestringOTime zone (e.g. Asia/Seoul). An unrecognized name is calculated in the default zone and the response gets a warning (UNKNOWN_TIMEZONE)
genderstringO"male" or "female"
topicstringTo get a single topic: personality, wealth, career, love, health, relationships, advice. All seven topics if omitted
langstringko (default) · en · ja · th
layersstring[]Extra reading layers: "astro" (Western astrology — Sun, Moon and ascendant signs) · "ziwei" (Zi Wei Dou Shu — main stars of the Life palace + main stars of each topic's palace). Appended after each topic's Saju paragraphs, astrology first, then Zi Wei Dou Shu. Omitted or [] leaves the response exactly as it is now
calendar · isLeapMonthstring · booleanWith "lunar", year, month and day are read as a lunar date and converted to solar for calculation (default "solar"). isLeapMonth: true means a leap month; if that year has no such leap month, 400 INVALID_LUNAR_DATE (since 2026-10-12). Lunar requests get lunarInput in the response (same format as the Manseryeok API)
birthLatitude · birthLongitudenumberBirthplace coordinates (send both). The Saju paragraphs and layers use the same true solar time correction. The standard meridian is taken from the standard time of timezone (excluding daylight saving time). The ascendant needs both the birth time and coordinates
countrystringTwo-letter country code of birth. Sending KR, JP or TH without coordinates corrects with that country's representative longitude (no correction for other countries). layers also use this country (inferred from lang if omitted)
zaSiRulestringDay pillar for the Ja hour (after 23:00): "traditional" (default, next day's day pillar) · "yajaSi" (night Ja hour: same day's day pillar; after location correction, the hour-pillar stem in the 23:00 hour is based on the next day's Day Master, since 2026-10-20). Used only for the Saju paragraphs (Zi Wei Dou Shu always rolls over to the next day at 23:00)
birthTimePrecisionstringexact · approximate · estimated · unknown. Location correction applies only with exact. If omitted and you send coordinates or country, it is treated as exact

Request example

{
  "year": 1990, "month": 9, "day": 4, "hour": 14, "minute": 30,
  "timezone": "Asia/Seoul", "gender": "male",
  "topic": "career", "lang": "ko"
}

Response example

{
  "success": true,
  "data": {
    "topics": [
      {
        "topic": "career",
        "paragraphs": [
          { "component": "ilju", "key": "임신", "text": "..." },
          { "component": "ilgan_wolji", "key": "임_신", "text": "..." },
          { "component": "ilgan_strength", "key": "임_신강", "text": "..." },
          { "component": "ilgan_yongshin", "key": "임_화", "text": "..." },
          { "component": "ohaeng_state", "key": "목_lack", "text": "..." },
          { "component": "ohaeng_state", "key": "토_lack", "text": "..." },
          { "component": "ohaeng_state", "key": "수_lack", "text": "..." },
          { "component": "top_sipsin", "key": "인성", "text": "..." }
        ]
      }
    ],
    "lang": "ko",
    "fallbackLanguage": null,
    "engine": { "version": 1, "styleVersion": "v1" }
  }
}
  • topics always follow the seven-topic order above; paragraphs within a topic follow the part order (day pillar → Day Master × month branch → strength → Yongsin → Five Elements state → Ten Gods group).
  • key is the Korean determination value (independent of language). There are 0–3 ohaeng_state paragraphs depending on the chart.
  • Language: if a paragraph does not exist yet in the requested language, that paragraph alone comes in Korean and fallbackLanguage: "ko" is set.
  • Time correction: without coordinates or country, the input time is used as is (same as the Manseryeok API). To get the same paragraphs as the INTA app (Korean, time entered directly), send country: "KR". For people born abroad, timezone alone does not correct the time — send coordinates as well.
  • Change on 2026-10-11: when coordinates are sent, the Saju paragraphs are now also corrected to true solar time (previously only layers used it). This aligns Saju, astrology and Zi Wei Dou Shu on the same time basis within one response, so calls that send coordinates may see different Saju paragraphs. Responses to calls that send neither the new fields nor coordinates are unchanged.

Western astrology and Zi Wei Dou Shu layers (layers)

Same structure as the Lifetime Saju in the INTA app. For each topic, astrology adds paragraphs for the Sun, Moon and ascendant signs, and Zi Wei Dou Shu (Purple Star) adds one paragraph for the main-star combination of the Life palace (命宮), followed by the main-star paragraph of the palace in charge of that topic — wealth (wealth) = Wealth palace (財帛), work (career) = Career palace (官祿), love (love) = Spouse palace (夫妻), health (health) = Health palace (疾厄). The personality, relationships and advice topics get only the Life palace paragraph. The first paragraph of each layer carries a label (e.g. "별자리로 보면") you can use as a subheading on screen. If you set topic, the layers add paragraphs for that topic only.

{
  "year": 1990, "month": 9, "day": 4, "hour": 14, "minute": 30,
  "timezone": "Asia/Seoul", "gender": "male",
  "birthLatitude": 37.5665, "birthLongitude": 126.978,
  "topic": "career", "lang": "ko",
  "layers": ["astro", "ziwei"]
}
{
  "success": true,
  "data": {
    "topics": [
      {
        "topic": "career",
        "paragraphs": [
          { "component": "ilju", "key": "임신", "text": "..." },
          ...
          { "component": "top_sipsin", "key": "인성", "text": "..." },
          { "component": "astro_sun", "key": "vir", "text": "...", "label": "별자리로 보면" },
          { "component": "astro_moon", "key": "pis", "text": "..." },
          { "component": "astro_asc", "key": "sag", "text": "..." },
          { "component": "ziwei_ming", "key": "tt_jm", "text": "...", "label": "자미두수로 보면" },
          { "component": "ziwei_palace", "key": "cr_tj", "text": "..." }
        ]
      }
    ],
    "lang": "ko",
    "fallbackLanguage": null,
    "engine": { "version": 1, "styleVersion": "v1" },
    "missingSections": 0,
    "layers": {
      "astro": { "included": true, "costWon": 20, "note": null },
      "ziwei": { "included": true, "costWon": 20, "note": null }
    }
  }
}
  • key: for astrology, a three-letter English abbreviation (vir = Virgo, etc.); for Zi Wei Dou Shu, a combination of the Life palace main-star abbreviations (tt_jm = Tian Tong + Ju Men; empty if there is no main star).
  • ziwei_palace: the per-topic palace paragraph. key is palace abbreviation_main-star abbreviation (wl = Wealth · cr = Career · sp = Spouse · hl = Health; e.g. cr_tj = Career palace, Tian Ji). A palace with two main stars gives two paragraphs; a palace with no main star (empty palace) gives none. It comes right after the Life palace paragraph and has no label (the first palace paragraph gets a label only when the topic has no Life palace paragraph). The price stays at +₩20 for the Zi Wei Dou Shu layer.
  • Without a birth time: the Zi Wei Dou Shu layer is left out and layers.ziwei returns "included": false, "reason": "birth_time_required" and a notice (note). For astrology, only the Sun (and the Moon, if its sign is certain) is added, calculated for noon.
  • Without coordinates: the ascendant paragraph is left out and layers.astro.note gives the reason. If country is not sent, the layers infer the birth country from lang (ko → Korea, ja → Japan, th → Thailand).
  • reason is one of birth_time_required · chart_unavailable · narrative_unavailable; layers that are left out are not billed.
  • missingSections: the number of paragraphs that should have been included but were left out because their reading text does not exist yet (base topics + added layers). 0 means nothing is missing. The price is the same even if some are missing.

Pricing

  • All seven topics ₩100/call; with topic set, ₩20/call. Only successful calls are billed, and repeated calls with the same input are each billed (idempotency).
  • layers: +₩20 for each layer that actually added paragraphs (same for all topics or one topic). E.g. all topics + astrology + Zi Wei Dou Shu = ₩140. The balance is checked in advance against the amount including the requested layers.
  • Input errors (400), insufficient balance (402) and paragraph lookup failures (503 lifetime_narrative_unavailable) are not billed. The usage-log endpoint is lifetime.
  • Input options: birth input is the same as the Manseryeok API (calendar, isLeapMonth, coordinates, country, zaSiRule, birthTimePrecision). Manseryeok-only options (astrology, glossary, name) are not accepted; sending them returns 400 (not billed).
  • If this feature is not enabled for the account, 404 lifetime_not_enabled (not billed).

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404lifetime_not_enabledThe feature is not enabled for the account
400INVALID_LUNAR_DATE · INVALID_LOCATIONNon-existent lunar date / only one of latitude and longitude sent
402INSUFFICIENT_BALANCEInsufficient balance when the amount including the requested layers was checked in advance
503lifetime_narrative_unavailableReading paragraphs could not be loaded (not billed)

Zi Wei Dou Shu Chart API

POST /api/v1/partners/v1/ziwei-chart

Computes a Zi Wei Dou Shu (紫微斗數, Purple Star astrology) chart from the date and hour of birth. It returns the 12 palaces (palace name, Heavenly Stem, Earthly Branch, decade-period ages), the 14 major stars and the auxiliary stars (with brightness and birth-year transformations), the Five Elements bureau, the Life and Body palace positions, the birth-year Four Transformations (sihua), and the decade and annual periods for the reference date. The same input always gives the same result. Labels are filled in the requested language; key is a fixed English value that does not depend on the language.

Calculation settings (fixed)

  • Leap months: days 1–15 of a lunar leap month count as that month; from day 16 on they count as the next month.
  • Births at 23:00–23:59: calculated as the Zi hour of the next day (a notice goes in notes.lateZiNotice).
  • Year boundary: lunar New Year's Day (Seollal). For births between Ipchun (start of spring) and Seollal, the year stem-branch can differ from Saju (Four Pillars of Destiny), which uses Ipchun; in that case a notice is added in chart.yearBasisNote.
  • Time correction: same rules as Saju. By default the input time is used as is (same as the Manseryeok API). If you send birthTimePrecision: "exact" together with coordinates (birthLatitude·birthLongitude) or a countryCode (KR·JP·TH), the time is corrected to true solar time. The result is in clock.
  • Transformations: Sanhe school star placement + birth-year Four Transformations. The engine is pinned to iztro 2.6.1 (chart.engine).

Request parameters

ParameterTypeRequiredDescription
year · month · daynumberOBirth year (1900-2100), month, day
hournumberOBirth hour (0-23). A Zi Wei Dou Shu chart cannot be built without the birth hour — if omitted, you get 400 birth_time_required
minutenumberBirth minute (0-59), 0 if omitted
genderstringO"male" or "female" (sets the direction of the decade periods)
calendarstringsolar (default) or lunar
isLeapMonthbooleantrue for a lunar leap month. Ignored for solar input. If that year has no such leap month, the regular month is used and chart.lunarDate.isLeapMonth comes back false
birthTimePrecisionstringexact (true solar time correction) or approximate (no correction + notes.approximateTimeNote). No correction if omitted
birthLatitude · birthLongitudenumberBirthplace coordinates (for exact correction)
countryCodestringCountry of birth, ISO 2-letter code, uppercase only (e.g. KR — kr returns 400). Used to correct with a representative longitude when no coordinates are sent
birthTimezoneOffsetnumberUTC offset of the birth time (minutes, 540 = KST)
referenceDatestringReference date for decade/annual periods, YYYY-MM-DD (default: today, Korea time)
langstringko (default) · en · ja · th
timezonestringFor compatibility with the Manseryeok API (not used in the calculation, so no warning is added even if the name is not recognized)
readingsbooleanIf true, adds readings to the response (paragraphs on the Life-palace major-star combination, 7 topics, and the major stars of the Wealth, Career, Spouse and Health palaces, in the lang language). Extra ₩20 (₩70 total). If omitted, the response is unchanged

Request example

{
  "year": 1990, "month": 5, "day": 15, "hour": 14, "minute": 30,
  "gender": "female", "referenceDate": "2026-10-08", "lang": "ko"
}

Response example (partial)

{
  "success": true,
  "data": {
    "engineVersion": 1,
    "lang": "ko",
    "referenceDate": "2026-10-08",
    "clock": {
      "inputSolarDate": "1990-05-15", "inputTime": "14:30",
      "correctedSolarDate": "1990-05-15", "correctedTime": "14:30",
      "correctionMinutes": 0, "dstApplied": false, "basis": null,
      "hourBranch": { "key": "wei", "label": "미" }
    },
    "chart": {
      "engine": { "name": "iztro", "version": "2.6.1", "config": { ... } },
      "solarDate": "1990-05-15",
      "lunarDate": { "year": 1990, "month": 4, "day": 21, "isLeapMonth": false },
      "yearGanzhi": "경오",
      "yearBasisNote": null,
      "fiveElementsClass": { "key": "earth5", "label": "토오국" },
      "soulPalaceBranch": { "key": "xu", "label": "술" },
      "bodyPalaceBranch": { "key": "zi", "label": "자" },
      "palaces": [
        ...,
        {
          "key": "ming", "label": "명궁",
          "branch": { "key": "xu", "label": "술" },
          "stem": { "key": "bing", "label": "병" },
          "isBodyPalace": false,
          "majorStars": [
            { "key": "ziwei", "label": "자미", "brightness": { "key": "de", "label": "득" }, "mutagen": null },
            { "key": "tianxiang", "label": "천상", "brightness": { "key": "de", "label": "득" }, "mutagen": null }
          ],
          "minorStars": [
            { "key": "lingxing", "label": "영성", "brightness": { "key": "miao", "label": "묘" }, "mutagen": null }
          ],
          "decadal": { "from": 5, "to": 14, "isCurrent": false }
        },
        ...
      ],
      "mutagens": [
        { "type": "lu", "label": "화록", "starKey": "taiyang", "starLabel": "태양", "palaceKey": "children", "palaceLabel": "자녀궁" },
        { "type": "quan", "label": "화권", "starKey": "wuqu", "starLabel": "무곡", "palaceKey": "wealth", "palaceLabel": "재백궁" },
        { "type": "ke", "label": "화과", "starKey": "taiyin", "starLabel": "태음", "palaceKey": "children", "palaceLabel": "자녀궁" },
        { "type": "ji", "label": "화기", "starKey": "tiantong", "starLabel": "천동", "palaceKey": "health", "palaceLabel": "질액궁" }
      ],
      "currentDecadal": { "palaceKey": "children", "from": 35, "to": 44 },
      "yearly": { "palaceKey": "wealth", "year": 2026 }
    },
    "features": [
      "bright:紫微=得", "co:紫微+天相@命", "cs:命=胎", "dir:逆", "hua:祿=太陽@子女",
      "lhua:忌=廉貞@流年", "limit:大限@子女", "limit:小限@辰", "ming:戌", "mingzhu:祿存",
      "star:天姚@辰", "star:紫微@命", "year:庚", "yy:陽女", ...
    ],
    "notes": { "lateZiNotice": null, "approximateTimeNote": null }
  }
}

Send the same request with "lang": "ja" and only the labels change — e.g. "yearGanzhi": "庚午", Life palace "label": "命宮", major stars "紫微"·"天相", bureau "土五局", transformation "化禄".

  • palaces always has 12 entries, sorted by Earthly Branch from Yin to Chou (Yin, Mao, Chen, Si, Wu, Wei, Shen, You, Xu, Hai, Zi, Chou). Tell palace types apart by key (ming, siblings, spouse, children, wealth, health, travel, friends, career, property, fortune, parents).
  • mutagens lists the birth-year Four Transformations (Lu, Quan, Ke, Ji) and the palace each transformed star sits in. The mutagen on a star object is also the birth-year transformation.
  • features is a sorted array of strings stating the facts that hold for this chart as Zi Wei Dou Shu rule keys (<kind>:<body>, using classical Chinese tokens) — about 1,200 per chart, about 27KB. It covers star positions, brightness, birth-year transformations, same palace, opposite palace, the three-directions-and-four-squares, 夾 (flanking), palace stems, the bureau, Life/Body palaces, Nayin, yin-yang, decade direction, the period palaces and the stars in them, plus things the chart itself does not list separately: minor-star positions, the Twelve Life Stages, minor periods, decade/annual transformations, the Life and Body rulers, and the Laiyin palace. That lets you match rule conditions with plain string comparison. Two things are left out because of size: three-star co·meet (co:A+B+C = co:A+B and co:A+C; same for meet) and relation keys involving minor stars (count them from star:<minor star>@<branch> and pal:·shen:·limit:: opposite palace +6, three-directions-and-four-squares 0·±4·+6, 夾 ±1).
  • Original-text quotes in rule source cards: in the sources of Zi Wei Dou Shu rule cards (Interpretation KB, approved partners only), a quote that could not be confirmed in the public original text (public-domain edition) — match: "PARTIAL" with no original-text location foundAt — is returned without the original text (quote) and location (location) and with "citation": "pending" (source being verified). The rule statement and sourceId stay as they are, and quotes confirmed in the original (match: "EXACT"), PARTIAL quotes with a confirmed location (foundAt), and engine evidence do not change. This keeps unverified quotes from looking like confirmed sources.
  • Traditional-edition quotes (match: "QUOTED"): quotes from traditional editions without a PD anchor. The original text (quote) and location (location) are returned as is. Quotes that show signs of editorial correction or collation do not fall under this.
  • currentDecadal is null before the starting age of the first decade period. yearly.year is the lunar year used for the annual period (the previous year if before Seollal).
  • With readings: true, readings = { ming, topics[], palaces[], lang, costWon, missingSections, version }. ming is the paragraph for the Life-palace major-star combination (key·text·lang), topics holds a paragraph per topic (topic·text·lang), and palaces holds a paragraph per palace and major star (palace·star·text·lang). A paragraph with no text in the requested language comes in Korean, and its lang is ko. missingSections is the number of paragraphs left out because no reading text exists yet (0 means all present). The extra charge is the same even if some are missing. version marks the edition of the reading dictionary the paragraphs came from (e.g. nr-3f9a0c1d2e4b). The same value means the same text; it changes when the dictionary text is revised. Only compare it; do not parse its format.

When the birth hour is missing (400)

{
  "success": false,
  "statusCode": 400,
  "code": "birth_time_required",
  "message": "Zi Wei Dou Shu needs the birth hour (hour 0-23). ...",
  "partial": {
    "birthYearMutagens": [
      { "type": "lu", "label": "화록", "starKey": "taiyang", "starLabel": "태양" },
      ...
    ],
    "yearStemStars": [
      { "key": "lucun", "label": "녹존", "branch": { "key": "shen", "label": "신" } },
      ...
    ]
  }
}

Only what is fixed without a birth hour comes back in partial: the stars of the birth-year transformations and the branches of the stars set by the year stem (Lucun, Qingyang, Tuoluo, Tiankui, Tianyue). Palace placement needs the hour, so do not make it up. This response is not charged.

Pricing

  • ₩50/call. With readings: true, +₩20 (₩70 total). Only successful calls are charged, and repeated calls with the same input are each charged (idempotency).
  • Input errors (400 invalid_birth_date·birth_time_required, etc.) and insufficient balance (402) are not charged. The usage-log endpoint is ziwei_chart.
  • Other errors that are not charged: 404 ziwei_not_enabled (feature not enabled for the account) · 404 ziwei_readings_not_enabled (reading paragraphs not enabled) · 503 ziwei_readings_unavailable (reading paragraphs could not be loaded).

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404ziwei_not_enabledThe feature is not enabled for the account
404ziwei_readings_not_enabledreadings: true but reading paragraphs are not enabled
400invalid_birth_dateA solar date that does not exist, or a lunar day / leap month out of range
400birth_time_requiredNo hour — partial comes with it, as in the example above
400reference_date_before_birthreferenceDate is before the birth date
402INSUFFICIENT_BALANCEInsufficient balance
503ziwei_readings_unavailableReading paragraphs could not be loaded

None of these are charged.

Saju Identity (included in Manseryeok)

Since 2026-10-10, Saju Identity content is returned in the identity field of the Manseryeok API response — Samjae (three-year misfortune cycle, samjae), Sinsal (symbolic stars, sinsal_list), Gongmang (void, gongmang), rooting (rooting_hints), seasonal strength (season_strength_hint), the 10-year/annual luck timeline (timeline) and Saju MBTI (mbti_inference). There is no charge beyond the Manseryeok price. The existing POST /api/v1/partners/v1/identity keeps working for now for partners who already integrated it. New integrations should use Manseryeok.

  • Deprecation headers: /identity responses carry Deprecation: true and Link: </api/v1/partners/v1/manseryeok>; rel="successor-version". An end date (Sunset) has not been set yet.
  • How it differs from Manseryeok's identity: keys already present in the Manseryeok response (saju·sipsin_map·strength·yongshin·gyeokguk·cheongan_detail·hap_chung_report·heoja_report·gongmang_detail·ohaeng_byeonsu_hits) are left out of identity. Read the same values from the Manseryeok top-level fields (wonkuk·strength·yongshinDetail·gyeokgukDetail, etc.).
  • currentYear·windowSize: Manseryeok does not accept these two parameters (sending them returns 400). If you need to change the timeline's base year or range, keep using /identity for now.
  • Ten Gods names are code values: the values of sipsin_map (stems and branches of the year, month and hour, and the day branch), such as 비견·정관, are Korean code values regardless of lang. For display names, send glossary: true in the Manseryeok request and use the translations in the returned glossary (glossary, in the lang language). For the full list, see Valid values > Language-independent code values.
  • Unknown birth time: /identity calculates with noon, while Manseryeok's identity uses the same three pillars (year, month, day) as the Manseryeok body. When /identity calculates on three pillars (birthHour.basis = three_pillars), saju.hour is { "cheongan": "", "jiji": "", "ohaeng": null } (since 2026-10-16 ohaeng is null, meaning there is no hour pillar). The hour pillar is not included in the Five Elements counts.
  • Pricing: the existing /identity is ₩10/call (only when enabled for the account; otherwise 404 identity_not_enabled). Manseryeok's identity has no extra charge. The usage-log endpoint is identity.

POST /api/v1/partners/v1/identity (legacy endpoint)

The request body is the same as the Manseryeok API, plus currentYear (1900~2200) and windowSize (1~10). If timezone is not recognized, warnings are added to the response, as with Manseryeok.

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/identity \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "year": 1990, "month": 5, "day": 15, "hour": 12, "minute": 0, "timezone": "Asia/Seoul", "gender": "male" }'

Response example (excerpt, actual engine output)

{
  "success": true,
  "data": {
    "saju": {
      "year":  { "cheongan": "경", "jiji": "오", "ohaeng": "금" },
      "month": { "cheongan": "신", "jiji": "사", "ohaeng": "금" },
      "day":   { "cheongan": "경", "jiji": "진", "ohaeng": "금" },
      "hour":  { ... }
    },
    "sipsin_map": { "year_cheongan": "비견", "year_jiji": "정관", "month_cheongan": "겁재", ... },
    "strength": { "dayMaster": "경", "score": -60, "result": "신약", "level": "중약", ... },
    "rooting_hints": { ... },
    "season_strength_hint": { "season": "여름", "strength_label": "최약(最弱)", ... },
    "sinsal_list": [ { "id": "shinsal_haksang", "name": "학당", "name_hanja": "學堂", "category": "길신", ... } ],
    "gongmang": { "sun": "갑술순", "void_branches": ["신", "유"], "hits": [] },
    "samjae": { "is_in_samjae": false, "current_year_branch": "오" },
    "yongshin": { "method": "조후", "primary": "수", ... },
    "timeline": { "current": { "daeun": { "startAge": 27, "endAge": 37, "stem": "갑", "branch": "신", ... }, ... } },
    "gyeokguk": { "primary": "비견격", ... },
    "mbti_inference": { "e_i": 48, "s_n": -8, "t_f": 16, "j_p": -18, "inferred": "ESFJ", ... },
    ...
  }
}

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404identity_not_enabledNot enabled for the account
400INVALID_LUNAR_DATE · INVALID_LOCATIONA lunar date that does not exist / only one of latitude and longitude sent
402INSUFFICIENT_BALANCEInsufficient balance

Saju ↔ MBTI Comparison API

POST /api/v1/partners/v1/saju-mbti

Send birth details together with the person's MBTI and you get back the natal chart (four pillars, or three if the birth time is unknown) plus a comparison that puts the tendency seen in the Saju next to the MBTI you entered for each of the four MBTI axes (E·I / S·N / T·F / J·P). It only calculates; no LLM. The price is ₩10/call, the same as Saju Identity (charged only on success).

This interpretation is a modern interpretive practice, not classical Saju theory. Since kbVersion 2026.10.16-2, the interpretation rules are the literature edition, checked against modern literature (response interim: false, kbStatus: "literature"). Both fields were kept so the response shape does not change. Interim rules without literature support were retired, and retired rule IDs are not reused. It does not judge which side is right or wrong, and it does not judge compatibility between types.

  • Birth input: accepts the same fields as the Manseryeok API (year·month·day·hour·minute·timezone·gender·calendar·isLeapMonth·location correction, etc.). It does not accept currentYear·windowSize (sending them returns 400).
  • mbti (required): a 4-letter string ("INFJ", case-insensitive, E/I · S/N · T/F · J/P per position) or an object of integers 0~100 per axis, { "E_I": 30, "S_N": 45, "T_F": 80, "J_P": 60 }. 100 is the first letter (E·S·T·J), and 50 or more reads as the first letter. A 4-letter string is converted to 75 for the given first letter and 25 for the second, i.e. treated as "slight" strength.
  • Strength (band): if |value − 50| is under 10, balanced; 10~25, slight; over 25, clear.
  • Saju-side tendency (sajuLean): for each axis, the weights of the rules that match the Day Master, the abundance or lack of each of the Five Elements (engine judgment), and a prominent Ten Gods group (3 or more positions of the same group) are added up per letter, and the larger side is chosen. If they are equal or no rule matches, it is balanced. Matched rule IDs are in ruleIds·matchedRules.
  • Comparison (comparison): same direction gives relation: "same" ("same direction as the Saju"), a different one gives "different" ("developed a side different from the Saju"), and if either side is balanced it is "neutral". The sentences (text) are currently Korean only. lang affects only birthHour.note.
  • Unknown birth time: if you leave out hour, it calculates on the same basis as Saju Identity and adds birthHour. On the three-pillar basis, wonkuk.hour is null and pillarCount is 3.
  • Caveat rule: caveatRuleId (R-MBTI-CAVEAT-001) and caveats contain a notice that this is a modern interpretation, not a judgment. Show it on screen together with the results.
  • Usage-log endpoint: saju-mbti

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/saju-mbti \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{ "year": 1990, "month": 5, "day": 15, "hour": 12, "minute": 0, "timezone": "Asia/Seoul", "gender": "male", "mbti": "INFJ" }'

Response example (excerpt, actual engine output)

{
  "success": true,
  "data": {
    "wonkuk": {
      "year":  { "cheongan": "경", "jiji": "오", "ohaeng": "금" },
      "month": { "cheongan": "신", "jiji": "사", "ohaeng": "금" },
      "day":   { "cheongan": "경", "jiji": "진", "ohaeng": "금" },
      "hour":  { "cheongan": "임", "jiji": "오", "ohaeng": "수" }
    },
    "pillarCount": 4,
    "dayMaster": "경",
    "strength": "신약",
    "ohaengCount": { "목": 0, "화": 3, "토": 1, "금": 3, "수": 1 },
    "ohaengState": { "목": "lack", "화": "balanced", "토": "lack", "금": "balanced", "수": "lack" },
    "dominantSipseong": ["관성"],
    "mbti": {
      "type": "INFJ",
      "source": "letters",
      "axes": {
        "E_I": { "value": 25, "letter": "I", "band": "slight" },
        "S_N": { "value": 25, "letter": "N", "band": "slight" },
        "T_F": { "value": 25, "letter": "F", "band": "slight" },
        "J_P": { "value": 75, "letter": "J", "band": "slight" }
      }
    },
    "sajuLean": {
      "E_I": { "lean": "balanced", "weights": { "E": 1, "I": 1 }, "ruleIds": ["R-MBTI-OHAENG-010", "R-MBTI-STRENGTH-002"] },
      "S_N": { "lean": "S", "weights": { "S": 1 }, "ruleIds": ["R-MBTI-OHAENG-006"] },
      "T_F": { "lean": "T", "weights": { "T": 2 }, "ruleIds": ["R-MBTI-STEM-007"] },
      "J_P": { "lean": "J", "weights": { "P": 1, "J": 2 }, "ruleIds": ["R-MBTI-OHAENG-008", "R-MBTI-SIPSEONG-002"] }
    },
    "comparison": [
      { "axis": "E_I", "label": "에너지 방향(E·I)", "user": "I", "userBand": "slight", "saju": "balanced", "relation": "neutral",
        "text": "에너지 방향(E·I): 사주에서는 어느 한쪽으로 기우는 경향이 뚜렷하지 않아요. 입력은 I 쪽이에요." },
      { "axis": "S_N", "label": "인식 방식(S·N)", "user": "N", "userBand": "slight", "saju": "S", "relation": "different",
        "text": "인식 방식(S·N): 사주와 다른 면을 키운 것으로 볼 수 있어요. 사주는 S 쪽 경향이 있고, 입력은 N 쪽이에요." },
      { "axis": "T_F", ... },
      { "axis": "J_P", "label": "생활 방식(J·P)", "user": "J", "userBand": "slight", "saju": "J", "relation": "same",
        "text": "생활 방식(J·P): 사주와 같은 방향이에요. 사주에서 보이는 J 쪽 경향이 입력에도 드러나요." }
    ],
    "matchedRules": [
      { "id": "R-MBTI-STEM-007", "statement": "庚 일간은 단단한 쇠의 기운으로 읽혀, 기준을 세워 따져 보고 결단하는 쪽(T)으로 기울기 쉬운 경향이 있어요." },
      ...
    ],
    "caveats": [ { "id": "R-MBTI-CAVEAT-001", "statement": "사주와 MBTI를 잇는 이 해석은 고전 명리 이론이 아니라 현대의 해석 관행이에요. ..." }, ... ],
    "caveatRuleId": "R-MBTI-CAVEAT-001",
    "interim": false,
    "kbStatus": "literature",
    "kbVersion": "2026.10.16-2",
    "engineVersion": "saju-mbti-1"
  }
}

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
400(body validation)mbti missing, not 4 letters, axis value not an integer 0~100, unknown key
400INVALID_LUNAR_DATE · INVALID_LOCATIONA lunar date that does not exist / only one of latitude and longitude sent
402INSUFFICIENT_BALANCEInsufficient balance
404saju_mbti_not_enabledThe feature has been turned off by operations

Today's Saju API

POST /api/v1/partners/v1/today

Matches the day pillar of a given date (default: today, Korea time) against the person's Saju and returns a daily summary (day pillar, today's Ten Gods, today-mode MBTI, this year's flow score, active Sinsal (symbolic stars), Samjae) and today's fortune (relation between the day pillar and the Day Master; lucky color, food and keywords; good hours and hours to avoid; suggested activities; a one-line message). It is the same calculation as the "Today" card on the home screen of the INTA – Life is Timing app. Activity fit (taskAffinity) and a one-line daily flow (summary.daily_flow) are always included. If you send temperature and weather, today's outfit (fashion) is added, and with mbtiBehavior: true, the today-mode MBTI behavior pattern (mbtiBehavior) is added too.

Request parameters

Accepts the same birth details and options as the Manseryeok API (lang, lunar dates, coordinates, zaSiRule, etc.). Stem-branch and Five Elements codes in the response (summary.day_master·ilju, etc.) are in Korean regardless of language — see the code values table. If timezone is not recognized, warnings are added to the response, as with Manseryeok. In addition:

ParameterTypeRequiredDescription
datestringDate to look at, YYYY-MM-DD (1900-2100). If omitted, today in Korea time
temperatureCnumberCurrent temperature (℃, −40~50). If sent, fashion (morning, daytime and evening outfits) is added to the response. If omitted, there is no fashion (it is not estimated)
weatherstringWeather: clear · cloudy · rain · snow · storm
humiditynumberRelative humidity (%, 0~100). fashion.mood (a one-line weather comment per time of day) is added only when all three of temperatureC·weather·humidity are sent
mbtiBehaviorbooleanIf true, adds the behavior pattern (mbtiBehavior) for summary.today_mbti. Default false

Request example

{
  "year": 1990, "month": 5, "day": 15, "hour": 12, "minute": 0,
  "timezone": "Asia/Seoul", "gender": "male",
  "date": "2026-10-10",
  "temperatureC": 16, "weather": "cloudy", "humidity": 60,
  "mbtiBehavior": true
}

Response example (actual engine output, partly omitted)

{
  "success": true,
  "data": {
    "date": "2026-10-10",
    "summary": {
      "ilju": "경진",
      "day_master": "경",
      "gyeokguk": "비견격",
      "gyeokguk_reason": "월지 사 중기(경) 투간 [연간=경]",
      "inferred_mbti": "ESFJ",
      "today_mbti": "ESFJ",
      "current_mbti": "ESFJ",
      "strength_label": "신약",
      "yongshin": "수",
      "today_date": "2026-10-10",
      "today_ganji": "정사",
      "today_day_ohaeng": "화",
      "today_sipsin": "정관",
      "flow_score": 60,
      "flow_tier": "good",
      "top_favored_task_type": "routine",
      "top_caution_task_type": "movement",
      "active_sinsal_names": ["학당", "암록", "복성귀인", "화개살", "자형(오오)"],
      "in_samjae": false,
      "daily_flow": "오늘(정사일) 화 에너지가 흘러 소통·확장에 유리해."
    },
    "omen": {
      "relation": { "type": "GEUK", "label": "조율의 날" },
      "color": { "name": "주황", "hex": "#FFA45B", "ohaeng": "화" },
      "food": "고추장비빔밥",
      "keyword": "공개",
      "message": "표현하고 싶은 마음과 주변 반응이 엇갈릴 수 있는 날이에요. 즉흥적인 말보다 한 박자 늦춘 표현이 좋아요.",
      "luckyHour": "오전 11시 ~ 오후 1시",
      "avoidHour": "밤 11시 이후",
      "activity": "칭찬 먼저 하기"
    },
    "taskAffinity": [
      { "taskType": "focus", "score": -0.25, "verdict": "neutral",
        "reason": "길 십신 2개 일치 · 흉 십신 2개 일치 · 일진 천간 정관(흉) · 일진 지지 편관(흉) · 천간합" },
      { "taskType": "creative", "score": -0.75, "verdict": "caution", "reason": "..." },
      ...,
      { "taskType": "routine", "score": 0.25, "verdict": "favored",
        "reason": "길 십신 4개 일치 · 일진 천간 정관(흉) · 일진 지지 편관(흉) · 천간합" }
    ],
    "fashion": {
      "temperatureC": 16, "tempBand": "T3",
      "main": "버건디 자켓", "accent": "가죽 벨트",
      "colorAttachLabel": "반지나 메탈 액세서리에",
      "byTime": {
        "morning": { "main": "버건디 자켓", "accent": "가죽 벨트", "tempBand": "T3" },
        "noon": { ... }, "evening": { ... }
      },
      "mood": {
        "feelsLikeC": 16, "tempBand": "jacket", "humidBand": "normal", "weatherKey": "cloudy",
        "morning": { "headline": "흐린 자켓 새벽", "body": "햇볕 없어 살짝 서늘. 자켓 한 벌.", "carry": [] },
        "noon": { ... }, "evening": { ... }
      }
    },
    "mbtiBehavior": {
      "mbti": "ESFJ",
      "headline": "협력·돌봄",
      "actions": ["약속·일정 챙기기 좋은 날", "주변에 도움 줄 수 있는 일 1개", "집·환경 정돈", "내 페이스도 지키기 — 다 떠안지 않기"],
      "closingLine": "주변과 결이 맞아 흐름이 부드러워요."
    }
  }
}
  • omen.relation.type is the code for the relation between your Day Master and the Heavenly Stem of that day's pillar: HAB (combination) · SAENG (generating) · SAME (same element) · GEUK (controlling) · CHUNG (clash). label is the human-readable name.
  • flow_score·flow_tier describe the flow of that year (annual luck, Seun), and today_mbti is a "today mode" that adds the day pillar to your Saju MBTI.
  • taskAffinity: today's fit for 7 activity types (focus·creative·contact·finance·organize·movement·routine). Each item is { taskType, score, verdict, reason }, and verdict is favored · neutral · caution. The order is the engine's order (not sorted by score).
  • summary.daily_flow: a one-line daily flow. summary.samjae_label: the Samjae type and stage in a Samjae year (absent otherwise).
  • fashion: only when temperatureC is sent. fashion.mood: only when temperatureC·weather·humidity are all sent. mbtiBehavior: only with mbtiBehavior: true (omitted if there is no MBTI pattern for that day).
  • If you send lang (en/ja/th), the summary's Gyeokguk (chart structure) and flow, today's fortune, and the taskAffinity·daily_flow·samjae_label·fashion·mbtiBehavior strings are all translated. Omitted or ko means Korean.
  • The same birth details and the same date (and the same weather input) always give the same result. An unknown birth time is calculated with noon, and birthHour is added. Lunar input adds lunarInput.

Pricing

  • ₩10/call. Only successful calls are charged, and repeated calls with the same input are each charged (idempotency).
  • Input errors (400), insufficient balance (402) and calculation failures are not charged. The usage-log endpoint is today.

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404today_not_enabledThe feature is not enabled for the account
400INVALID_LUNAR_DATE · INVALID_LOCATIONA lunar date that does not exist / only one of latitude and longitude sent
402INSUFFICIENT_BALANCEInsufficient balance

Dream Interpretation API

POST /api/v1/partners/v1/dream

Finds symbol words (e.g. sea, fish) in a dream an end user describes and returns them in order of prominence. The words come from HRUM's own dream symbol glossary (about 1,000 everyday nouns plus their synonyms, inflected forms and colloquial forms). The glossary holds words only — no dream meanings and no good/bad-omen judgments. Every word returned actually appears in the dream text, as the word itself or a synonym or inflected form.

Request parameters

ParameterTypeRequiredDescription
dreamstring✓The dream (up to 1,000 characters)
feelingstringHow the dream felt (up to 300 characters, e.g. "무섭고 답답했어요" — "it was scary and stifling"). Combined with the dream text to extract tags (up to 1,000 characters of the combined text are used)
langstringResponse language. Only ko is supported for now (the dictionary is Korean-only). Any other value returns 400
includeNegativeboolean Whether to include records of bad-omen judgments (death, illness, loss, etc.) in the classical sources. Default false. Ignored until classical sources are available

Request example

{
  "dream": "바다에서 커다란 물고기를 두 손으로 잡는 꿈을 꿨어요"
}

Response example

{
  "success": true,
  "data": {
    "lang": "ko",
    "mode": "glossary",
    "items": [
      { "keywords": ["바다"], "score": 1 },
      { "keywords": ["물고기"], "score": 0.979 },
      { "keywords": ["손"], "score": 0.749 }
    ]
  }
}
  • items holds up to 40 entries. One entry = keywords (one base word from the symbol glossary; e.g. "치아" (teeth) and "앞니" (front teeth) both become ["이빨"]) + score. Each word appears only once. Entries are ordered by prominence in the dream (earlier, more frequent and more specific words come first), and score is 0~1, equal or decreasing down the list (1 = the most prominent symbol in this dream).
  • mode: glossary (found in the symbol glossary) · none (no symbol found). feeling is searched together with the dream text.
  • The same input gives the same result. It is calculated directly on the server, with no external calls.
  • No interpretation text is included. If you need an interpretation to show end users, pass the dream text and these words to AI Saju Reading (only: "dream") or into your own LLM input.
  • Changed 2026-10-11: the old title·meaning fields were removed and replaced by keywords. On the same day, the source of symbol words changed to HRUM's own symbol glossary, so each entry now has one word and mode has two values, glossary · none (the old values tags · fallback_char no longer appear).

Pricing

  • ₩30/call. Charged only when at least one entry is found; an empty result (items: []) costs ₩0. Repeated calls with the same input are each charged (idempotency).
  • Input errors (400) and insufficient balance (402) are not charged. The usage-log endpoint is dream.

Classical sources kbMatches

Returns the dream-interpretation KB rules that match things in the dream (sky, snake, market, etc.). The sources are original texts of old dream books in the public domain — the Dunhuang manuscript 『周公解夢書』 (Duke of Zhou's Book of Dream Interpretation, P.3908, 10th century) and the 夢部 (dreams section) of 『古今圖書集成』. The same dream text always gives the same result (character-matching rules; no tagging service or LLM).

"kbMatches": [
  {
    "ruleId": "R-DREAM-DRAGON-002",
    "area": "DRAGON",
    "polarity": "positive",
    "status": "verified",
    "statement": "『주공해몽서』(돈황본 P.3908, 第十二 龍蛇章)는 뱀(虵=蛇)이 길에 있는 꿈(夢見虵當道者)에 대해 「大吉」라고 적는다(글자대로: 크게 길하다).",
    "quote": {
      "text": "夢見虵當道者大吉",
      "sourceId": "SRC-DREAM-P3908",
      "source": "『周公解夢書』 (敦煌 P.3908)",
      "location": "P.3908 第十二 龍蛇章 f09/左/行3下",
      "foundAt": "dream-pd/dunhuang-p3908-bnf/P3908-TRANSCRIPTION-merged.md:198"
    },
    "caveatRuleIds": [],
    "matchedTerms": ["구렁이", "길"]
  }
]
  • statement reports what the original text recorded. It is not a prophecy that the dream foretells anything. quote is the original line it is based on, with its location.
  • polarity: positive · negative · neutral, based on the judgment part of the original. negative (records of death, illness, loss, etc.) is included only with includeNegative: true.
  • caveatRuleIds: caveat rules attached to that rule (e.g. records of death or illness carry R-DREAM-CAVEAT-002).
  • An empty array if no rule matches. Not attached to calls that are not charged (e.g. a ₩0 call with items: []).

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404dream_not_enabledThe feature is not enabled for the account
402INSUFFICIENT_BALANCEInsufficient balance

An empty dream or one over 1,000 characters gets the common 400. If no symbol is found, it is not an error: you get 200 with items: [] (₩0).

Face & Palm Measurement API

No photos are sent. Send the coordinates extracted with MediaPipe on the partner's device or browser (478 face points / 21 hand points, and palm-line polylines), and the server computes measurement keys (ratios, angles, zones) with the same pure functions and returns the face-reading and palm-reading KB rules (records from old original texts) that match those values. No LLM is used, and the same coordinates always give the same result. Reference code for extracting coordinates (browser MediaPipe, Node, curl) is on the reference code page. If any of model name, version, normalization or point count differs from the values below, you get 400.

POST /api/v1/partners/v1/face-measure

ParameterTypeRequiredDescription
modelName, modelVersionstring✓The model that produced the coordinates. Outside the allowed list: 400 MEASURE_MODEL_UNSUPPORTED / MEASURE_MODEL_VERSION_MISMATCH. Current: face_landmarker · float16/[email protected]
normalizationstring✓face-norm-1
landmarksnumber[][]✓Exactly 478 [x, y] or [x, y, z] (divided by image width and height, −0.02~1.02)
imageWidth, imageHeightnumber✓Size in pixels
hairline[number, number]One hairline point at the center of the face. The server does not estimate it — without it, keys related to 上停 (upper face zone) are null (reason hairline_missing). We recommend having the user tap where the hair starts above the forehead (hair segmentation models are for reference only)
qualityobjectyawDeg·pitchDeg·rollDeg·blendshapes (MediaPipe coefficient name → score). Quality checks run only if you send it
includeNegativebooleanWhether to include records of bad-omen judgments (polarity negative). Default false (same name and meaning as in Dream Interpretation)

POST /api/v1/partners/v1/palm-measure

ParameterTypeRequiredDescription
modelName, modelVersionstring✓Current: hand_landmarker · float16/[email protected]
normalizationstring✓palm-norm-1
landmarksnumber[][]✓Exactly 21
imageWidth, imageHeightnumber✓Size in pixels
handednessstring✓left | right — the subject's actual hand (let the user choose)
mirroredbooleantrue for a horizontally flipped photo (selfie preview)
creasesobject[]Palm-line polylines (up to 64, 2~512 points per line). Fields: points (✓), label (life·head·heart·fate·sun·mercury·simian·unknown), kind (segment | branch), autoLabel. Without it, line-related keys are null (creases_missing)
autoLabelbooleanNames every unnamed line with the server's shared heuristic. To use it on one line only, set autoLabel on that entry. A label you set yourself always wins
includeNegativebooleanSame as for the face. Default false
  • Join line pieces before sending. The server does not join pieces, and the heuristic names only one piece per line. A piece cut where it crosses another line (e.g. a heart line meeting the fate line) gets a name only if you join it first, as the reference code stitch_creases.py does. Leave real, visible breaks broken (the number of breaks is counted by the breaks key).

Request example (face, shortened)

{
  "modelName": "face_landmarker",
  "modelVersion": "float16/[email protected]",
  "normalization": "face-norm-1",
  "landmarks": [[0.5012, 0.3121, -0.031], … 478개 …],
  "imageWidth": 1080,
  "imageHeight": 1350,
  "hairline": [0.501, 0.182],
  "quality": { "yawDeg": 2.1, "pitchDeg": -3.4, "blendshapes": { "eyeBlinkLeft": 0.05, "jawOpen": 0.02 } }
}

Response example (opening excerpt)

{
  "success": true,
  "data": {
    "normalization": "face-norm-1",
    "keys": { "face.thirds.upper_ratio": 0.3333, "face.brow.length_vs_eye": 1.625, "face.brow.outer_overhang": 0.1648, … },
    "reasons": {},
    "quality": { "rollDeg": 0, "iodPx": 158, "flags": [] },
    "kbMatches": [
      {
        "ruleId": "R-FACE-PALACE-031",
        "area": "PALACE",
        "kind": "interpretation",
        "polarity": "positive",
        "status": "verified",
        "statement": "『古今圖書集成』 相術部彙考 「十二宮訣」 兄弟는 눈썹이 눈보다 길게 지나가면(眉長過目) …라고 적는다 …",
        "quote": { "text": "眉長過目,三四兄弟", "sourceId": "SRC-FACE-HUIKAO", "source": "…", "location": "…", "foundAt": "…" },
        "caveatRuleIds": ["R-FACE-CAVEAT-001"],
        "matchedConditions": [{ "key": "face.brow.outer_overhang", "op": "gt", "value": 0, "actual": 0.1648 }]
      }
    ]
  }
}
  • keys: all measurement keys (key definitions, formulas and confidence are in the key table document). If a value cannot be produced it is null, with the reason in reasons: hairline_missing · blink · smile · jaw_open · finger_bent · creases_missing · back_of_hand (photo of the back of the hand — line-related keys only), etc.
  • missingSections: the number of measurement keys whose value is null (keys and reasons are in keys·reasons). 0 means every key was produced.
  • kbMatches: only rules where the original text itself gives a number or comparison. Rules that would be judged only by thresholds we set are left out until their basis is verified (PoC). statement is a record of what the original text says, not a prophecy or diagnosis. negative only with includeNegative: true.
  • Quality: if the head is turned or tilted more than 10° (MEASURE_QUALITY_POSE) or the face is too small (MEASURE_QUALITY_FACE_SIZE), the whole request returns 400 — ask the user to retake the photo. Closed eyes, smiling or an open mouth do not return 400; only the affected keys are null.
  • Input errors come back as 400 with a code (MEASURE_LANDMARK_COUNT · MEASURE_COORD_RANGE · MEASURE_COORD_NAN · MEASURE_CREASE_LIMIT, etc.). Error messages contain only point indexes, never coordinate values.
  • When showing bad-omen records (includeNegative: true): statement records what the original text says. Present it to users as "the text says there is such a tendency + here is how you might handle it", and do not turn it into words that belittle a person or label their character.

Error codes

HTTPcodeMeaning
400MEASURE_MODEL_UNSUPPORTEDmodelName is not on the allowed list
400MEASURE_MODEL_VERSION_MISMATCHmodelVersion is not on the allowed list (currently only float16/[email protected])
400MEASURE_NORMALIZATION_MISMATCHnormalization is not face-norm-1 / palm-norm-1
400MEASURE_LANDMARK_COUNTPoint count is not 478 for a face / 21 for a hand
400MEASURE_LANDMARK_SHAPE · MEASURE_COORD_NAN · MEASURE_COORD_RANGE · MEASURE_IMAGE_SIZE · MEASURE_DEGENERATEErrors in point format, numbers, range (−0.02~1.02) or image size
400MEASURE_HANDEDNESS · MEASURE_CREASE_LIMIT · MEASURE_CREASE_SHAPE · MEASURE_CREASE_LABELErrors in hand or palm-line input
400MEASURE_QUALITY_POSE · MEASURE_QUALITY_FACE_SIZEHead turned more than 10° · face too small — retake the photo
400(validation error)A field not in the documentation was sent — send only allowed fields
402INSUFFICIENT_BALANCEInsufficient credit balance
404face_measure_not_enabled · palm_measure_not_enabledFeature not open yet

No error response is charged.

Personal data

  • We do not receive photos. Photos are turned into coordinates on the user's device, and only the coordinates reach the API.
  • We do not store coordinates. Coordinates are face and hand geometry, so the server uses them only in memory while handling the request and does not keep them in logs, databases or error messages. Usage logs record only the endpoint, status and amount.
  • Please obtain end-user consent on the partner side.
  • MediaPipe, used by the reference code, is licensed under Apache-2.0 and may send usage statistics. When you ship it in a product, distribute the license notice with it and reflect it in your privacy policy.

Pricing

  • Face measurement ₩30/call, palm measurement ₩30/call (excl. VAT · ₩33 incl. VAT).
  • Charged only when at least one measurement key is produced. No measurement keys means ₩0, and input/quality errors (400), insufficient balance (402) and features not yet open (404) are not charged either. The usage-log endpoints are face-measure / palm-measure.
  • If palm measurement judges the photo to show the back of the hand (back_of_hand in quality.flags), every key is null (reason back_of_hand), the KB rules are empty, and nothing is charged (₩0). Send again with a palm-side photo and the correct handedness·mirrored. The back-of-hand check compares the winding direction of the wrist, index-finger base and little-finger base points against handedness·mirrored.
  • The price is the same even if some keys are null. The number of missing keys is given in missingSections.

Face & Palm Reading API (photos)

Reads the face (face reading, physiognomy) from one face photo and the palm (palm reading) from one palm photo. Photos are handled only in server memory and are not stored. For face reading, you can send the same coordinates (landmarks) as Face Measurement instead of a photo — the measurement keys and KB rules are then found without a photo, and AI writes a plain-language reading for each rule (face reading always includes the AI reading; if you only need measurement without AI, use Face Measurement). Results are a reading of traditional face and palm reading for fun and reference, not statements of fact or a diagnosis.

Reading text is currently Korean only; the answer is in Korean even if you send a different lang (the response lang is always "ko").

POST /api/v1/partners/v1/face-reading

ParameterTypeRequiredDescription
imageobjectOne of the two{ data, mimeType } — data is the base64 of the photo bytes (up to 8MB decoded), mimeType is image/jpeg · image/png · image/webp. The format is checked again from the bytes (400 if different)
landmarksobjectOne of the twoThe Face Measurement request body as is (modelName · modelVersion · normalization · landmarks 478 points · imageWidth · imageHeight · hairline · quality · includeNegative)
consentobject✓ for photosConsent to photo processing — see the consent object below
llmbooleanNo need to send it — the AI reading is always included. Sending false returns 400 USE_FACE_MEASURE (measurement without AI is Face Measurement)
langstringAccepts ko · en · ja · th, but reading text is currently Korean only (response lang = "ko")
endUserIdstringThe end-user ID the partner uses (up to 255 characters). Recorded only in usage logs

Send exactly one of image and landmarks. Sending both or neither returns 400 READING_INPUT_ONE_OF.

Request example (face, photo)

{
  "image": { "data": "/9j/4AAQSkZJRgABAQ…(base64)", "mimeType": "image/jpeg" },
  "consent": { "photo": true, "version": "photo-consent-2026-10", "subject": "end_user" },
  "lang": "ko",
  "endUserId": "member-1024"
}

POST /api/v1/partners/v1/palm-reading

ParameterTypeRequiredDescription
imageobject✓One palm-side photo. Same format and size limits as face reading
consentobject✓Consent object
creasesobject[]Palm lines the user traced over the photo (up to 16). Fields: line (life · head · heart · fate · sun · mercury · simian · unknown), points (2~256 [x, y], 0~1 from the top-left of the photo). Used first when locating the lines
langstringSame as face reading (reading text currently Korean only)
endUserIdstringSame as face reading

Request example (palm, traced lines)

{
  "image": { "data": "/9j/4AAQSkZJRgABAQ…(base64)", "mimeType": "image/jpeg" },
  "consent": { "photo": true, "version": "photo-consent-2026-10", "subject": "end_user" },
  "creases": [
    { "line": "life", "points": [[0.42, 0.38], [0.36, 0.52], [0.34, 0.71]] },
    { "line": "heart", "points": [[0.78, 0.33], [0.6, 0.31], [0.47, 0.34]] }
  ]
}

Example: "consent": { "photo": true, "version": "photo-consent-2026-10", "subject": "end_user" }

  • photo: must be true — it means the user agreed to photo processing.
  • version: the version of the consent text the partner showed (1~40 characters).
  • subject: end_user (a photo of the user) · third_party (a photo of someone else — the partner must also obtain that person's consent).
  • Missing or invalid values return 400 CONSENT_REQUIRED. Usage logs record only version and subject.

Photo processing

  • Photos are used only in memory while the request is handled. They are not kept on disk, in databases, logs or file storage, and no face or hand feature values (templates, embeddings) are created or stored. We do not identify who someone is or match against other photos.
  • Before sending to the external model, metadata such as EXIF (including location and device info), XMP and ICC is removed, and the photo is re-encoded as JPEG with the long side at most 1600px. WebP has only its metadata removed and is sent as is, so only WebP with a long side of 1600px or less is accepted (larger returns 413 — shrink it or send JPEG/PNG).
  • Each photo takes two external model calls: the photo is first classified (whether it is a face or palm, one person, appears to be a minor, image quality), and the reading is written only if it passes. Both calls use store: false.
  • Photos that appear to show a minor (under 18) are not read. The same goes for photos with several people or two or more hands, and photos that are not of a face or palm (400 IMAGE_NOT_ALLOWED). A photo of the back of the hand returns 400 PALM_NOT_DETECTED. For palm reading, have only one palm in the frame. Even a little of the other hand's fingertips showing may be seen as two hands and rejected.
  • External model: sent to OpenAI (USA), with store: false. OpenAI does not use API input for training and keeps it for up to 30 days for abuse monitoring before deleting it. The response's meta.processing states the same thing in a fixed form.
  • Readings do not include health, illness or lifespan; race, ethnicity, nationality or skin color; age; crime or morality; identity or lookalikes; sexual orientation or religion; or definitive statements about character or destiny. The model is instructed not to, and such sentences are filtered out of the response once more.

The response's meta.processing has a fixed form (for photo readings):

"processing": {
  "provider": "OpenAI",
  "model": "gpt-5.6-luna",
  "region": "US",
  "store": false,
  "retention": "API 입력은 학습에 쓰지 않음, 오남용 감시로 최대 30일 보관 뒤 삭제",
  "imageStored": false
}

In responses that did not call the external model (a landmarks call that ended at ₩0 because no measurement key or matching KB record was found), provider · model · region · store · retention are null and imageStored is false.

Response example (face, photo)

{
  "success": true,
  "data": {
    "items": [
      {
        "key": "forehead",
        "label": "이마",
        "observation": "이마가 넓고 둥글게 보여요.",
        "reason": "넓은 이마는 생각을 넓게 펼치는 쪽으로 읽혀요. 떠오른 계획을 글로 정리해 두면 장점이 더 살아나요."
      },
      {
        "key": "eyes",
        "label": "눈",
        "observation": "눈매가 길고 가로로 뻗어 있어요.",
        "reason": "긴 눈매는 사람과 상황을 찬찬히 살피는 쪽으로 읽혀요."
      }
    ],
    "summary": "생각을 넓게 펼치고 찬찬히 살피는 인상으로 읽혀요.",
    "lang": "ko",
    "meta": {
      "input": "image",
      "processing": { "provider": "OpenAI", "model": "gpt-5.6-luna", "region": "US", "store": false, "retention": "API 입력은 학습에 쓰지 않음, 오남용 감시로 최대 30일 보관 뒤 삭제", "imageStored": false },
      "disclaimer": "entertainment_reference",
      "disclaimerText": "전통 관상·손금·성명학을 재미와 참고로 풀어 본 내용이에요. 사실 판단이나 진단이 아니에요.",
      "kbRuleIds": []
    }
  }
}
  • items: only features visible in the photo are included. Face key: face_shape · forehead · eyebrows · eyes · nose · mouth · ears · chin. Palm key: hand_shape · fingers · life_line · head_line · heart_line · fate_line · sun_line · mounts. label comes in Korean.
  • A face reading sent as landmarks also returns the Face Measurement response fields (normalization · keys · reasons · quality · kbMatches · missingSections) under the same names, and items has one entry per KB rule. As in a photo reading, label is the feature name (eyebrows · eyes · nose …) and reason is the AI's plain-language reading, together with key (measurement key) · ruleId · polarity · source (the KB rule statement the reading is based on). meta.kbRuleIds collects the source rule IDs.
  • meta.disclaimer is always entertainment_reference. Show meta.disclaimerText (or wording with the same meaning) on the user's screen.

Error codes

HTTPcodeMeaning
400READING_INPUT_ONE_OFFace: not exactly one of image and landmarks
400IMAGE_REQUIREDPalm: no image
400CONSENT_REQUIREDA photo was sent but consent is missing or invalid
400IMAGE_INVALID · IMAGE_FORMAT_UNSUPPORTEDUnreadable photo · mimeType differs from the actual format · not JPEG, PNG or WebP (including animated WebP)
413IMAGE_TOO_LARGEPhoto over 8MB · over 8192px or 16.8 megapixels · WebP long side over 1600px
400IMAGE_NOT_ALLOWEDAppears to be a minor · several faces / two or more hands · not a face or palm photo
400FACE_NOT_DETECTED · PALM_NOT_DETECTEDNo face / palm found (a photo of the back of the hand is PALM_NOT_DETECTED)
400USE_FACE_MEASUREllm: false with face landmarks — measurement without AI is the Face Measurement API
400MEASURE_LOW_QUALITYBlurry · dark · obscured · too small · tilted — the reason is in the response's quality. Ask the user to retake the photo
400CREASE_INVALIDA palm line has fewer than 2 points or points outside 0~1
400MEASURE_*landmarks input errors (same codes as Face Measurement)
402INSUFFICIENT_BALANCEInsufficient balance — checked before calling the external model
404face_reading_not_enabled · palm_reading_not_enabledFeature not open yet
409IDEMPOTENCY_KEY_REUSED · IDEMPOTENCY_IN_PROGRESSThe same X-Idempotency-Key was used with a different body · the first request with the same key is still being processed (see Retries and resends below)
429FILTERED_QUOTA_EXCEEDEDToday's filtered photos reached the daily limit — see Daily limit on filtered photos below
502READING_UNAVAILABLEThe reading could not be produced (including truncated output) — please try again
503IMAGE_BUSYPhoto processing is busy — please try again shortly
504LLM_UPSTREAM_TIMEOUTThe external model timed out

Billing

  • Prices are in the pricing table. Only 200 responses are charged; all 4xx/5xx errors and failures are ₩0 (including filtered photos and the 502 for truncated output). A landmarks call with no measurement keys or matching KB records is also ₩0.
  • The usage-log endpoints are face-reading / palm-reading.
  • The photo APIs can be called with server keys only (app-only "AI connection" keys get 403 APP_KEY_SCOPE). Send photos through the partner's server.

Daily limit on filtered photos

Photos filtered out after classification (IMAGE_NOT_ALLOWED · FACE_NOT_DETECTED · PALM_NOT_DETECTED · MEASURE_LOW_QUALITY) and 502s from truncated output cost the partner ₩0 but still incur external model costs. So each partner can have, per Korea-time day (00:00~24:00), at most the larger of 200 and three times the number of successful photo readings that day. Beyond that, we answer with 429 FILTERED_QUOTA_EXCEEDED (₩0, with filtered · limit in the body) before calling the external model, and the limit resets at 00:00 the next day (KST). Showing users photo tips first (face forward, good light, one person, palm side) reduces filtered photos. landmarks calls are not counted.

Retries and resends (X-Idempotency-Key)

When resending after a network drop, add an X-Idempotency-Key header (1~128 characters, printable ASCII). If the same partner resends the same key with the same body (photo included) within 24 hours, the original result is returned as is — no new charge, no external model call, and the response carries the header Idempotent-Replayed: true.

  • Same key with a different body: 409 IDEMPOTENCY_KEY_REUSED (₩0) — use a new key for a new request.
  • The first request with the same key is still in progress: 409 IDEMPOTENCY_IN_PROGRESS (₩0) — resend with the same key after a few seconds.
  • Only charged 200 results are stored. If the response was an error or ₩0, resending with the same key processes it again.
  • We store only a hash of the key, a hash of the body and the encrypted result, and delete them after 24 hours. Photos are not stored.
  • Without the header, each request is processed and charged as before.

Name Analysis API

Returns a Name Analysis (Korean name study, seongmyeonghak) reading from a name (in Hangul, plus Hanja if known) and, optionally, the date of birth. The response is split into deterministic, computed by code (sound elements, stroke counts, the four 81-number patterns, character-meaning elements, Yongsin (favorable element) support), and interpretation, written by AI based only on those values. Each calculation carries the IDs of the Name Analysis KB rules it is based on. Results are a reading of traditional name study for fun and reference.

  • Name reading (default): calculation + AI interpretation. The name is sent to OpenAI (USA), so consent is required.
  • Name calculation ("interpret": false): calculation only; AI is not called. interpretation is null and consent is not needed. The two are priced differently (pricing).
  • Reading text is currently Korean only; the answer is in Korean even if you send a different lang (response lang = "ko").

POST /api/v1/partners/v1/naming

ParameterTypeRequiredDescription
name.hangulstring✓Hangul name of 2~6 syllables, surname first (e.g. 김하늘). 남궁 (Namgung), 황보 (Hwangbo), 제갈 (Jegal), 선우 (Seonu), 독고 (Dokgo), 사공 (Sagong), 서문 (Seomun) and 동방 (Dongbang) are treated as two-syllable surnames
name.hanja(string|null)[]One Hanja character per syllable, null if unknown. If the length differs from the number of syllables: 400 NAME_HANJA_LENGTH
name.strokes(number|null)[]Original stroke count (based on the original character form) per syllable (1~64), null if unknown (see the stroke note below)
name.meanings(string|null)[]The Hanja meaning per syllable (hun, e.g. 물 (water) · 쇠 (metal), 1~20 characters). Character-meaning elements are judged from this meaning only
birthobject{ year, month, day, hour?, minute?, gender?, timezone? } (solar calendar). If sent, the Saju Yongsin is calculated to see whether the name supplies that element. timezone defaults to Asia/Seoul. The date of birth is not sent to the external model
interpretbooleanDefault true (name reading). false means name calculation only (no AI)
echoNamebooleanAdded 2026-10-31. Default true (same as before). With false, the name's characters are not written back anywhere in the response — name is null, hangul·hanja·meaning in deterministic.syllables are null, and position (from 1) is added instead. Character mentions in items also switch to positions, like 「첫 글자(목(木))」 ("first character (Wood (木))"), and for a name reading the name itself is not sent to the AI (only calculated values and positions are sent)
consentobject✓ for name reading{ "version": "name-consent-2026-10", "subject": "end_user" } — version is the version of the consent text the partner showed (1~40 characters), subject is end_user · third_party. Missing or invalid values return 400 CONSENT_REQUIRED. Usage logs record only version and subject. Not needed with interpret: false
langstringAccepts ko · en · ja · th, but currently answers in Korean only
endUserIdstringThe partner's end-user ID (usage logs only)
  • Stroke counts are calculated as original stroke counts (based on the original character form) (e.g. 氵 counts as 水, 4 strokes). The only Hanja whose stroke counts the server knows are 38 surname characters (R-NAME-STROKE-004), and 11 of them whose values differ between dictionaries (洪·沈·河·池·鄭·郭·陳·權·禹·羅·尹) are left empty with strokeNote: "disputed". For any other Hanja, send the original stroke count in name.strokes to get the four patterns. If even one syllable has no stroke count, you get suri: null, suriMissing: "strokes_unknown".
  • Sound elements are set by the initial consonant, using the assignment guttural (ㅇ·ㅎ) = Earth, labial (ㅁ·ㅂ·ㅍ) = Water (R-NAME-SOUND-011).

Request example

{
  "name": { "hangul": "김민수", "hanja": ["金", "民", "秀"], "strokes": [null, 5, 7], "meanings": ["쇠", null, null] },
  "birth": { "year": 1990, "month": 5, "day": 17, "hour": 9 },
  "consent": { "version": "name-consent-2026-10", "subject": "end_user" }
}

To get the name calculation only, add "interpret": false; you can leave out consent. The response's interpretation is then null, and the external-model fields in meta.processing are null.

Response example (shortened)

{
  "success": true,
  "data": {
    "name": { "hangul": "김민수", "hanja": ["金", "民", "秀"] },
    "deterministic": {
      "surnameLength": 1,
      "syllables": [
        { "index": 0, "hangul": "김", "hanja": "金", "strokes": 8, "strokeSource": "kb", "strokeNote": null, "strokeRuleId": "R-NAME-STROKE-004", "soundElement": "wood", "soundRuleId": "R-NAME-SOUND-004", "meaning": "쇠", "rootElement": "metal" },
        { "index": 1, "hangul": "민", "hanja": "民", "strokes": 5, "strokeSource": "input", "strokeNote": null, "strokeRuleId": null, "soundElement": "water", "soundRuleId": "R-NAME-SOUND-010", "meaning": null, "rootElement": null },
        { "index": 2, "hangul": "수", "hanja": "秀", "strokes": 7, "strokeSource": "input", "strokeNote": null, "strokeRuleId": null, "soundElement": "metal", "soundRuleId": "R-NAME-SOUND-006", "meaning": null, "rootElement": null }
      ],
      "soundFlow": { "elements": ["wood", "water", "metal"], "relations": ["reverse_generating", "reverse_generating"], "generating": 0, "overcoming": 0, "score": 0.5 },
      "suri": {
        "won": { "sum": 12, "number": 12, "grade": "bad" },
        "hyung": { "sum": 13, "number": 13, "grade": "good" },
        "i": { "sum": 15, "number": 15, "grade": "good" },
        "jeong": { "sum": 20, "number": 20, "grade": "bad" }
      },
      "suriMissing": null,
      "yongshin": { "element": "wood", "supplied": true, "suppliedBy": ["sound"] },
      "kbRuleIds": ["R-NAME-JAWON-001", "R-NAME-MODERN-001", "R-NAME-MODERN-003", "R-NAME-SOUND-004", "R-NAME-SOUND-006", "R-NAME-SOUND-010", "R-NAME-SOUND-011", "R-NAME-STROKE-004"]
    },
    "interpretation": { "text": "첫 글자는 목, 가운데 글자는 수, 끝 글자는 금 기운이에요. 소리가 서로 부딪치지 않고 이어지는 이름으로 읽혀요." },
    "items": [
      { "key": "sound_flow", "label": "발음오행 흐름", "score": 0.5, "reason": "김(목(木)) → 민(수(水)) → 수(금(金)) · 상생 0 · 상극 0", "ruleId": "R-NAME-SOUND-011" },
      { "key": "suri_hyung", "label": "형격", "grade": "good", "score": 13, "reason": "획수 합 13 → 13수(길)", "ruleId": "R-NAME-MODERN-001" },
      { "key": "yongshin_match", "label": "용신 보완", "grade": "supplied", "reason": "용신 목(木)을 이름이 발음으로 채워 줘요" }
    ],
    "summary": "부드럽게 이어지는 소리에 용신 기운이 담긴 이름으로 읽혀요.",
    "lang": "ko",
    "meta": {
      "input": "name",
      "processing": { "provider": "OpenAI", "model": "gpt-5.6-luna", "region": "US", "store": false, "retention": "API 입력은 학습에 쓰지 않음, 오남용 감시로 최대 30일 보관 뒤 삭제", "imageStored": false },
      "disclaimer": "entertainment_reference",
      "disclaimerText": "전통 관상·손금·성명학을 재미와 참고로 풀어 본 내용이에요. 사실 판단이나 진단이 아니에요.",
      "kbRuleIds": ["R-NAME-JAWON-001", "R-NAME-MODERN-001", "R-NAME-MODERN-003", "R-NAME-SOUND-004", "R-NAME-SOUND-006", "R-NAME-SOUND-010", "R-NAME-SOUND-011", "R-NAME-STROKE-004"]
    }
  }
}
  • Element values: wood · fire · earth · metal · water. Relations: generating · overcoming (controlling) · same · reverse_generating (the later one feeds the earlier one).
  • 81-number grade: good (auspicious) · mixed (half good, half bad) · bad (inauspicious). The 81-number table of good and bad is a modern practice with no classical source (R-NAME-MODERN-001).
  • items carries deterministic in the same shape used elsewhere (key · label · grade · score · reason · ruleId). The four patterns are included only when all stroke counts are present.
  • Reading text does not quote the name's characters or go through them one by one (added 2026-10-31). Characters are referred to only by position, such as "first character · middle character · last character". The server checks the result, and if a sentence wraps a character of the name in quotes or writes out the whole name, it rewrites once with a stricter instruction (no extra charge). If any remain, those parts are replaced with position words (「‘김’과」 → 「첫 글자와」, i.e. "‘Kim’ and" → "the first character and") and 「이 이름」 ("this name") before returning. A single character written without quotes (e.g. a common character that works like a particle) cannot be told apart from ordinary words and is not replaced.
  • Response example with "echoName": false (added 2026-10-31, shortened) — use it when showing results without the name.
    "name": null,
    "deterministic": {
      "syllables": [
        { "index": 0, "position": 1, "hangul": null, "hanja": null, "strokes": 8, "strokeSource": "kb", "soundElement": "wood", "meaning": null, "rootElement": "metal", ... },
        { "index": 1, "position": 2, "hangul": null, "hanja": null, "strokes": 5, "soundElement": "water", "meaning": null, ... },
        { "index": 2, "position": 3, "hangul": null, "hanja": null, "strokes": 7, "soundElement": "metal", "meaning": null, ... }
      ], ...
    },
    "items": [
      { "key": "sound_flow", "label": "발음오행 흐름", "score": 0.5, "reason": "첫 글자(목(木)) → 가운데 글자(수(水)) → 끝 글자(금(金)) · 상생 0 · 상극 0", "ruleId": "R-NAME-SOUND-011" }, ...
    ]

Error codes

HTTPcodeMeaning
400NAME_INVALIDHangul name is not 2~6 syllables · format error in hanja/strokes/meanings values
400NAME_HANJA_LENGTHhanja length differs from the number of syllables
400CONSENT_REQUIREDName reading without consent, or with invalid values
400(validation error)A date that does not exist · a field not in the documentation
402INSUFFICIENT_BALANCEInsufficient balance
404naming_not_enabledFeature not open yet (name reading and name calculation separately)
409IDEMPOTENCY_KEY_REUSED · IDEMPOTENCY_IN_PROGRESSThe same X-Idempotency-Key was used with a different body · the first request with the same key is still being processed (see Retries and resends below)
502READING_UNAVAILABLEThe AI interpretation could not be produced — not charged (no partial charge for returning only the calculation). Please try again
504LLM_UPSTREAM_TIMEOUTThe external model timed out

Billing

  • Prices are in the pricing table (name calculation · name reading). Only 200 responses are charged; all 4xx/5xx errors and failures are ₩0. The usage-log endpoint for both is naming. It can also be called with app-only "AI connection" keys.

Retries and resends (X-Idempotency-Key)

When resending, add an X-Idempotency-Key header (1~128 characters, printable ASCII). If the same partner resends the same key with the same body within 24 hours, the original result is returned as is — no new charge, no AI call, and the response carries the header Idempotent-Replayed: true.

  • Same key with a different body: 409 IDEMPOTENCY_KEY_REUSED (₩0) — use a new key for a new request.
  • The first request with the same key is still in progress: 409 IDEMPOTENCY_IN_PROGRESS (₩0) — resend with the same key after a few seconds.
  • Only charged 200 results are stored, encrypted, together with a hash of the key and a hash of the body, for 24 hours. If the response was an error or ₩0, resending with the same key processes it again. Without the header, each request is processed and charged as before.
  • meta.disclaimer is entertainment_reference. Show meta.disclaimerText on the user's screen as well.

Tarot API

POST /api/v1/partners/v1/tarot

Set a number of cards (count, 1~10) or a spread (spread), and cards are drawn without repeats from a 78-card deck (22 major · 56 minor). For each card you get upright/reversed, keywords, meaning and the card image URL. There is no AI interpretation — the meaning is the fixed text for that card. That gives you three ways to use it.

Request parameters

ParameterTypeRequiredDescription
countnumber△Number of cards to draw, 1~10 (integer). Send only one of count and spread. Sending both or neither returns 400
spreadstring△Spread. Each has a fixed number of cards and position names: single (card of the day, 1 card) · past-present-future (past, present, future, 3 cards) · choice (choice: current situation, obstacle, outcome, 3 cards) · celtic-condensed (condensed Celtic, 5 cards) · future-prediction (looking ahead, 5 cards)
reversedbooleanAllow reversed cards (default true). With false, all cards are upright
seedstringA string for reproducibility (1~200 characters). The same seed + the same count/spread + the same reversed always draws the same cards. Without it, every draw is random (cryptographic randomness)
questionstringThe end user's question (up to 500 characters). Not used for interpretation; returned as is in the response — a convenience field for passing to a later LLM step
langstringLanguage of card names, keywords, meanings and spread names: ko (default) · en · ja · th

Request example

{
  "spread": "past-present-future",
  "question": "올해 하반기에 이직해도 될까요?",
  "seed": "user-1042:2026-10-10"
}
curl -X POST https://api.hrum.ai/api/v1/partners/v1/tarot \
  -H "X-Partner-Api-Key: $HRUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"count": 1, "lang": "en", "reversed": false}'

Response example (one card excerpted)

{
  "success": true,
  "data": {
    "lang": "ko",
    "count": 3,
    "spread": { "type": "past-present-future", "name": "과거·현재·미래", "description": "시간 흐름을 따라가는 질문", "positions": ["과거", "현재", "미래"] },
    "reversedAllowed": true,
    "seed": "hrum-example-1",
    "cards": [
      {
        "position": 1,
        "positionLabel": "과거",
        "id": "wands_10",
        "name": "완드의 10",
        "nameEn": "Ten of Wands",
        "arcana": "wands",
        "reversed": true,
        "keywords": ["내려놓기", "위임", "해방"],
        "meaning": "무거운 짐을 마침내 내려놓거나 도움을 받기 시작하는 시기.",
        "imageUrl": "https://console.hrum.ai/tarot/cards/v2/wands_10.webp"
      }
    ],
    "cardBackUrl": "https://console.hrum.ai/tarot/cards/v2/back.webp",
    "question": "올해 하반기에 이직해도 될까요?",
    "costPoints": 20
  }
}

Pricing

Waite source text kb

Once open, kb is added to the response. For each drawn card, it returns the card's upright and reversed meanings from the original text of A. E. Waite, The Pictorial Key to the Tarot (1910, public domain), with rule IDs (R-TAROT-*) and quotes from the original. meaning·keywords above stay as they are; only kb is added, and the price is the same.

{
  "method": "celtic-cross",
  "seed": "user-1042:2026-10-11"
}

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404tarot_not_enabledThe feature is not enabled for the account
404tarot_kb_not_enabledmethod was sent but the source-text feature is not open yet
402INSUFFICIENT_BALANCEInsufficient balance

Send exactly one of count·spread·method. Two or more, or none, gets the common 400.

Natal Chart API (Western astrology) Discontinued

Discontinued — use Astrology Total (/astrology). With the same birth input, you get 10 celestial bodies, houses, aspects and pre-written reading paragraphs in one call for ₩50. Existing /natal-chart calls keep working, but we do not recommend it for new integrations.

POST /api/v1/partners/v1/natal-chart

Computes a Western astrology natal chart (positions of 7 bodies — Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn — plus the Ascendant) from the date and time of birth. Uranus, Neptune and Pluto are not included. It uses the tropical zodiac of Western astrology, not the solar terms.

Request parameters

Parameter Type Required Description
birthDate string O Birth date YYYY-MM-DD (solar calendar). Real calendar dates only (Feb 30 etc. are rejected)
birthTime string Birth time HH:mm (24h). If birthTimePrecision is omitted, it is treated as exact and this time is used (since 2026-10-17). To not use a time, leave out birthTime or send birthTimePrecision: "unknown"
gender string Gender ("male" | "female"). Accepted for compatibility only; not used in the calculation
birthLatitude number Latitude -90~90 (needed for the Ascendant, together with longitude)
birthLongitude number Longitude -180~180
birthTimezoneOffset number UTC offset (minutes), default: 540 (KST)
birthTimePrecision string "exact" | "approximate" (to the hour) | "unknown". If omitted: exact when birthTime is present, otherwise unknown (calculated at noon)
readings boolean If true, readings (reading paragraphs on planetary dignities and aspects) is included in the response. Extra ₩20. If not enabled for the account: 404 natal_readings_not_enabled (not charged)
lang string Language of the readings paragraphs — ko (default) · en · ja · th

Response fields

Field Type Description
sun object Sun position { body, sign (Korean), signEn, element (Korean: 불·흙·공기·물 = fire, earth, air, water), degreeInSign, eclipticLongitude }
moon object Moon position
mercury object Mercury position
venus object Venus position
mars object Mars position
jupiter object Jupiter position
saturn object Saturn position
ascendant object|null Ascendant. Calculated when the birth time and coordinates are present and birthTimePrecision is exact (including omitted) or approximate; otherwise null
ascendantReason string Why the Ascendant is null (only when present, e.g. time precision is unknown or no coordinates)
ascendantHourBasis boolean true if the Ascendant was calculated from an approximate (hour-level) time — label it as "hour-level"
ascendantEstimate object|null With approximate, an Ascendant estimate over a ±60-minute window. window = { startLocal, endLocal, switchLocal, beforeSign, afterSign } (switchLocal is when the sign changes within the window, null if it does not)
moonSignUncertain boolean Included only when the time is unknown (unknown) — true if the Moon changes sign during that day (the noon-based Moon sign is not certain)
coordsBasis string|null Location used for the Ascendant: "coords" (the coordinates sent) or null (no coordinates / unknown time)
engineVersion string Calculation edition. Currently "natal-2" — marks that results can differ from older responses without this field (see the 2026-10-17 change below)
readings object Only with readings: true. dignities (readings for planets in domicile, exaltation, detriment or fall) · aspects (up to 2 of the most exact aspects, deviation = degrees off exact) · lang · costWon (extra charge) · missingSections (number of paragraphs left out because no reading text exists yet; 0 means all present — the extra charge is the same even if some are missing) · version (edition marker of the reading dictionary; the same value means the same text — for comparison)

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/natal-chart \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "birthDate": "1990-05-15",
    "birthTime": "12:00",
    "birthLatitude": 37.5665,
    "birthLongitude": 126.978,
    "birthTimezoneOffset": 540,
    "birthTimePrecision": "exact"
  }'

Response example

{
  "success": true,
  "data": {
    "sun": {
      "body": "Sun",
      "sign": "황소자리",
      "signEn": "Taurus",
      "element": "흙",
      "degreeInSign": 24.03,
      "eclipticLongitude": 54.0343
    },
    "moon": { "body": "Moon", "sign": "염소자리", "signEn": "Capricorn", "element": "흙", ... },
    "mercury": { "body": "Mercury", "sign": "황소자리", "signEn": "Taurus", ... },
    "venus": { "body": "Venus", "sign": "양자리", "signEn": "Aries", "element": "불", ... },
    "mars": { ... },
    "jupiter": { ... },
    "saturn": { ... },
    "ascendant": {
      "body": "Ascendant",
      "sign": "사자자리",
      "signEn": "Leo",
      "element": "불",
      "degreeInSign": 23.13,
      "eclipticLongitude": 143.1256
    },
    "coordsBasis": "coords",
    "engineVersion": "natal-2"
  }
}

Request for the example above: {"birthDate": "1990-05-15", "birthTime": "12:00", "birthLatitude": 37.5665, "birthLongitude": 126.978, "birthTimezoneOffset": 540, "birthTimePrecision": "exact"} (excerpt of actual engine output).

Reading paragraphs (readings: true)

Adds pre-written reading paragraphs for planetary dignities (domicile, exaltation, detriment, fall) and the 2 most exact aspects. They are the same paragraphs as on the Manseryeok screen of the INTA – Life is Timing app. Below is actual engine output (1990-09-04 14:30, Seoul coordinates, exact).

{
  "success": true,
  "data": {
    "sun": { ... }, "moon": { ... }, ...,
    "readings": {
      "dignities": [
        { "key": "Me_dom", "lang": "ko",
          "text": "수성이 본궁인 쌍둥이자리나 처녀자리에 있으면 정보를 모으고 분류해 말로 옮기는 기능이 막힘없이 또렷하게 드러나요. 생각이 많아질 때 하나를 골라 끝까지 파고들면 그 명료함이 실제 성과로 이어져요." },
        { "key": "Sa_dom", "lang": "ko",
          "text": "토성이 본궁인 염소자리나 물병자리에 있으면 책임을 지고 구조를 세워 반복 훈련으로 숙련을 쌓는 기능이 막힘없이 또렷하게 드러나요. 기준이 지나치게 굳어질 때 쉬어 갈 틈을 일정에 남겨 두면 그 인내가 성숙하게 쓰여요." }
      ],
      "aspects": [
        { "key": "Me_Sa_tr", "deviation": 0.04, "lang": "ko",
          "text": "수성과 토성의 트라인은 절제와 의지를 생각에 더해, 서두르지 않고 앞을 내다보며 구조를 짜는 데 힘이 실려요. 변화가 필요한 순간에 익숙한 방식만 고집할 수 있으니 새 방법도 한 번 시험해 보세요." },
        { "key": "Mo_Ma_sq", "deviation": 1.03, "lang": "ko",
          "text": "달과 화성이 스퀘어를 이루면 감정이 올라오는 순간 욱하거나 규칙에 반발하는 쪽으로 기울 수 있어요. 화가 난 뒤 반응을 한 템포 늦추는 연습을 하면 그 뜨거움이 지켜야 할 것을 지키는 힘으로 바뀌어요." }
      ],
      "lang": "ko",
      "costWon": 20
    }
  }
}
  • If the birth time is unknown (no birthTime, or birthTimePrecision: "unknown"), the Moon's position varies widely, so it is left out of the aspect calculation.
  • A paragraph with no text in the requested language comes in Korean, and its lang is ko.
  • If the reading paragraphs cannot be loaded: 503 natal_readings_unavailable (not charged).

Notes

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404natal_readings_not_enabledreadings: true but reading paragraphs are not enabled
503natal_readings_unavailableReading paragraphs could not be loaded
402(none)Balance is less than the call price — this API has no pre-call balance check, so you get a 402 with no code

Astrology Total API

Like Manseryeok, it returns all the astrology calculations in one call. The default is the full natal chart (10 bodies from the Sun to Pluto · lunar nodes · 4 angles · houses (Placidus by default) · aspects · dignities). Add blocks to the request and transits + secondary progressions (timing) and two-person synastry (partner) come in the same response. Use only the parts you need. No LLM is used. Besides numbers and rule keys (features), HRUM's pre-written reading paragraphs (readings) always come along at no extra charge. It is the same shared astrology engine as INTA, and body positions were checked against NASA/JPL Horizons and Swiss Ephemeris reference values (planets within 0.005°). All interpretive parameters (orbs, aspect types, dignity tables) come from rules in classical sources, and for values where the sources disagree (transit and progression orbs), there is no default — you choose (see "Defaults and values you must choose" below).

POST /api/v1/partners/v1/astrology

Birth input is the same as the Natal Chart API (birthDate · birthTime · birthTimePrecision · birthLatitude · birthLongitude · birthTimezoneOffset, default 540 = KST; Korean daylight saving time and the 1954~61 UTC+8:30 period are applied automatically). If birthTimePrecision is omitted, it is unknown (noon-based).

ParameterTypeRequiredDescription
houseSystemsstring[]Any of placidus · whole_sign · equal · porphyry. If omitted, ["placidus"]. For birthplaces inside the polar circles (latitude above about 66.56°, angles.polar), Porphyry is calculated instead of Placidus and the result carries "fallbackFrom": "placidus" (the same happens if you request Placidus explicitly)
minorAspectsbooleanInclude semi-sextile (30°), semi-square (45°), sesquiquadrate (135°) and quincunx (150°) (natal and synastry). Default false — the 5 major aspects only
orbsstringNatal and synastry orb table: leo1928 (default — conjunction/opposition Sun–Moon 12°, Sun/Moon–planet 10°, planet–planet 8°, square/trine 8°, sextile 7°, semi-square/sesquiquadrate 4°, semi-sextile/quincunx 2°) · heindel1919 (a single 6° for majors, conservative)
nodestringWhich node the rule keys Nn (North Node) and Sn (South Node = North Node + 180°) follow: mean (default, mean node) · true (true node). Applies to node_sign Nn_…·Sn_… in features and to node conjunction keys (Mo_Nn_cj·Nn_Asc_cj, etc.). MNn_…·TNn_… keys are both returned regardless of this value
langstringLanguage of signName (sign names) — ko (default) · en · ja · th
timingobjectIf sent, transits + secondary progressions are calculated and placed in the response's timing. See the table below
partnerobjectThe other person's birth input (same fields as above, except lang). If sent, the aspects and sign relations between the two people go into the response's synastry. If the other person's birth time and coordinates (birthLatitude·birthLongitude) are present, their Ascendant (Asc) and Midheaven (MC) are also calculated and included in the cross aspects

Inside the timing block:

ParameterTypeRequiredDescription
orbDegreesnumberOOrb (degrees) for transit→natal and progressed→natal aspects, 0.5~10. The same value is used for every body pair (5 major aspects only)
atstringThe moment to look at. YYYY-MM-DD (= 00:00 UTC that day) or a date-time with an offset (2026-10-11T09:00:00+09:00, ...Z). A date-time without an offset returns 400. If omitted, now
progressionsbooleanCalculate secondary progressions (default true)
yearLengthstringThe year length used to turn elapsed time into "years of life": tropical (365.24219 days, default) · julian (365.25 days)

Request example 1 — default (natal only)

{
  "birthDate": "1990-05-15",
  "birthTime": "14:30",
  "birthTimePrecision": "exact",
  "birthLatitude": 37.5665,
  "birthLongitude": 126.978,
  "houseSystems": ["placidus", "whole_sign"]
}
curl -X POST https://api.hrum.ai/api/v1/partners/v1/astrology \
  -H "X-Partner-Api-Key: $HRUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"birthDate": "1990-05-15", "birthTime": "14:30", "birthTimePrecision": "exact", "birthLatitude": 37.5665, "birthLongitude": 126.978, "minorAspects": true, "lang": "en"}'

Response example 1 (excerpt, actual engine output)

{
  "success": true,
  "data": {
    "natal": {
      "utc": "1990-05-15T05:30:00.000Z",
      "timePrecision": "exact",
      "timeKnown": true,
      "zodiac": "tropical",
      "lang": "ko",
      "bodies": [
        { "id": "Sun", "longitude": 54.1347, "sign": "Taurus", "signKey": "tau", "signName": "황소자리", "degreeInSign": 24.1347, "speed": 0.9643, "retrograde": false, "dignities": [], "house": { "placidus": 9, "whole_sign": 9 } },
        { "id": "Moon", "longitude": 293.8244, "sign": "Capricorn", "signKey": "cap", "signName": "염소자리", "degreeInSign": 23.8244, "speed": 12.2943, "retrograde": false, "dignities": ["det"], "house": { "placidus": 4, "whole_sign": 5 } },
        ...
      ],
      "nodes": [
        { "id": "MeanNode", "longitude": 311.3535, "sign": "Aquarius", "signName": "물병자리", "speed": -0.0529,
          "south": { "longitude": 131.3535, "sign": "Leo", "signName": "사자자리", ... } },
        { "id": "TrueNode", ... }
      ],
      "angles": { "asc": 173.5996, "mc": 82.8067, "dsc": 353.5996, "ic": 262.8067,
        "ascSign": { "sign": "Virgo", "signName": "처녀자리", ... }, "mcSign": { ... }, "dscSign": { ... }, "icSign": { ... }, "polar": false },
      "anglesOmitted": null,
      "houseSystems": ["placidus", "whole_sign"],
      "houses": {
        "placidus": { "cusps": [173.5996, 199.6989, 229.8749, ...], "bodies": { "Sun": 9, "Moon": 4, "Jupiter": 10, ... } },
        "whole_sign": { "cusps": [150, 180, 210, ...], "bodies": { "Sun": 9, "Moon": 5, ... } }
      },
      "housesExcluded": [],
      "orbs": "leo1928",
      "minorAspects": false,
      "aspects": [
        { "a": "Jupiter", "b": "Uranus", "type": "opposition", "code": "op", "key": "Ju_Ur_op", "separation": 179.6986, "orb": 0.3014, "orbLimit": 8, "strength": "exact", "applying": false, "outOfSign": false },
        { "a": "Sun", "b": "Moon", "type": "trine", "code": "tr", "key": "Su_Mo_tr", "orb": 0.3104, "orbLimit": 8, "strength": "exact", "applying": true, "outOfSign": false },
        ...
      ],
      "dignities": { "Sun": [], "Moon": ["det"], "Mercury": [], "Venus": ["det"], "Mars": [], "Jupiter": ["exa"], "Saturn": ["dom"] },
      "moonSignUncertain": false,
      "features": [
        { "type": "planet_sign", "key": "Su_tau" },
        { "type": "dignity", "key": "Mo_det" },
        { "type": "planet_retrograde", "key": "Me_rx" },
        { "type": "planet_house", "key": "Su_h9", "system": "placidus" },
        { "type": "aspect", "key": "Su_Mo_tr", "strength": "exact", "applying": true, "orb": 0.31 },
        { "type": "meta", "key": "time_known" },
        ...
      ]
    },
    "readings": {
      "dignities": [
        { "key": "Mo_det", "text": "달이 염소자리에서 손상 자리에 있으면 감정을 바로 내보이기보다 책임과 일의 질서 속에서 조심스럽게 다루는 경향이 있어요. ...", "lang": "ko" },
        { "key": "Ve_det", ... }, { "key": "Ju_exa", ... }, { "key": "Sa_dom", ... }
      ],
      "aspects": [
        { "key": "Su_Mo_tr", "text": "태양과 달의 트라인은 의지와 정서가 같은 곳을 바라봐, 스스로 길을 내며 생활 전반을 고르게 꾸려 가는 힘이 실려요. ...", "lang": "ko", "deviation": 0.31 },
        { "key": "Su_Sa_tr", ... }, { "key": "Mo_Sa_cj", ... }, ...
      ],
      "lang": "ko"
    },
    "readingsUnavailable": false,
    "costPoints": 50
  }
}

If you do not send timing·partner, the response has no timing·synastry keys at all.

Request example 2 — with timing (transits + secondary progressions)

{
  "birthDate": "1990-05-15",
  "birthTime": "14:30",
  "birthTimePrecision": "exact",
  "birthLatitude": 37.5665,
  "birthLongitude": 126.978,
  "houseSystems": ["whole_sign"],
  "timing": { "at": "2026-10-11T09:00:00+09:00", "orbDegrees": 3, "yearLength": "tropical" }
}
curl -X POST https://api.hrum.ai/api/v1/partners/v1/astrology \
  -H "X-Partner-Api-Key: $HRUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"birthDate": "1990-05-15", "timing": {"orbDegrees": 2, "progressions": false}}'

Response example 2 (excerpt; natal has the same shape as example 1)

{
  "success": true,
  "data": {
    "natal": { ... },
    "timing": {
      "at": "2026-10-11T00:00:00.000Z",
      "orbDegrees": 3,
      "natalPoints": ["Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", "Asc", "MC"],
      "transits": {
        "bodies": [ { "id": "Sun", "longitude": 197.6966, "sign": "Libra", "signName": "천칭자리", "speed": 0.9889, "retrograde": false, ... }, ... ],
        "aspects": [
          { "a": "Mars", "b": "Mercury", "type": "square", "key": "tMa_nMe_sq", "orb": 0.6358, "orbLimit": 3, "strength": "exact", "applying": true, "outOfSign": false },
          ...
        ]
      },
      "progressions": {
        "yearLength": "tropical",
        "yearLengthDays": 365.24219,
        "ageYears": 36.4081,
        "instant": "1990-06-20T15:17:39.048Z",
        "bodies": [ { "id": "Sun", "longitude": 89.0352, "sign": "Gemini", "signName": "쌍둥이자리", ... }, ... ],
        "aspects": [
          { "a": "Uranus", "b": "Mercury", "type": "trine", "key": "pUr_nMe_tr", "orb": 0.0862, "orbLimit": 3, "strength": "exact", "applying": false, "outOfSign": false },
          ...
        ]
      },
      "features": [
        { "type": "transit", "key": "tMa_nMe_sq", "strength": "exact", "applying": true, "orb": 0.64 },
        { "type": "progressed", "key": "pUr_nMe_tr", "strength": "exact", "applying": false, "orb": 0.09 },
        ...
      ]
    },
    "readings": { ... }, "readingsUnavailable": false,
    "costPoints": 50
  }
}

Request example 3 — with partner (synastry)

{
  "birthDate": "1990-05-15",
  "birthTime": "14:30",
  "birthTimePrecision": "exact",
  "birthLatitude": 37.5665,
  "birthLongitude": 126.978,
  "houseSystems": ["whole_sign"],
  "partner": { "birthDate": "1992-11-03", "birthTime": "08:10", "birthTimePrecision": "exact", "birthLatitude": 35.1796, "birthLongitude": 129.0756 }
}
curl -X POST https://api.hrum.ai/api/v1/partners/v1/astrology \
  -H "X-Partner-Api-Key: $HRUM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"birthDate": "1990-05-15", "birthTime": "14:30", "birthTimePrecision": "exact", "partner": {"birthDate": "1992-11-03"}}'

Response example 3 (excerpt; natal has the same shape as example 1)

{
  "success": true,
  "data": {
    "natal": { ... },
    "synastry": {
      "partner": {
        "utc": "1992-11-02T23:10:00.000Z",
        "timePrecision": "exact",
        "timeKnown": true,
        "moonSignUncertain": false,
        "bodies": [ { "id": "Sun", "longitude": 220.7903, "sign": "Scorpio", "signName": "전갈자리", "speed": 1.0016, "retrograde": false, "dignities": [], ... }, ... ],
        "angles": { "asc": 252.31, "mc": 175.86, "ascSign": { "sign": "Sagittarius", "signName": "사수자리", ... }, "mcSign": { "sign": "Virgo", "signName": "처녀자리", ... } },
        "anglesOmitted": null
      },
      "orbs": "leo1928",
      "minorAspects": false,
      "aspects": [
        { "a": "Moon", "b": "Mars", "type": "opposition", "code": "op", "key": "aMo_bMa_op", "orb": 0.2993, "orbLimit": 10, "strength": "exact", "applying": null, "outOfSign": false },
        { "a": "Neptune", "b": "Uranus", "type": "conjunction", "code": "cj", "key": "aNe_bUr_cj", "orb": 0.3898, "orbLimit": 8, "strength": "exact", "applying": null, "outOfSign": false },
        ...
      ],
      "features": [
        { "type": "syn_aspect", "key": "aMo_bMa_op", "strength": "exact", "orb": 0.3 },
        { "type": "syn_aspect", "key": "aNe_bUr_cj", "strength": "exact", "orb": 0.39 },
        { "type": "astro_syn", "key": "Su_Mo_se" },
        { "type": "syn_sign", "key": "aAsc_bAsc_es" },
        ...
      ]
    },
    "readings": { ... }, "readingsUnavailable": false,
    "costPoints": 50
  }
}

Defaults and values you must choose

Not provided

Pricing

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404astrology_not_enabledThe feature is not enabled for the account
400at_before_birthtiming.at is earlier than the birth time (when progressions are on)
402(none)Balance is less than the call price — this API has no pre-call balance check, so you get a 402 with no code

Fortune Flow API

From one person's Saju, it calculates how the 10-year luck cycles (Daeun), annual luck (Seun) and monthly luck layer onto the natal chart, and returns numbers for graphs plus a list of relations. One person per call; no LLM. The vertical-axis value is "the Day Master-side score with the luck characters added" — the natal chart's total strength score plus, for each luck character, a share scored with the same per-character table as the natal chart (support, control, hidden stems, roots), clipped to ±100. It does not measure whether fortune is good or bad (R-SAJU-UN-034). The labels strong, balanced and weak (Day Master strength) are those of the natal chart and are not reassigned by luck.

Price: ₩50 per call (same as Manseryeok; the same price even with a wider range or monthly luck). Failed responses such as 400 are not charged.

All numbers come from the same shared Saju engine as INTA. The birth time is read as Korean standard time wall-clock history (1954~61 UTC+8:30, daylight saving time applied automatically) and compared against the solar-term start, so results are the same regardless of server time zone.

Birthplace (added 2026-10-15, saju-flow-5): accepts the same fields as the Manseryeok API (birthLatitude·birthLongitude·country·birthTimePrecision) and applies true solar time correction the same way. When the birth time is close to an hour-pillar, day-pillar or solar-term boundary, the correction changes natal chart characters, so send Fortune Flow the same location you sent Manseryeok to make both results use the same natal chart. Without a location, it uses the same default handling as Manseryeok called without a location (currently no correction) and reports that default in the response's assumedLocation.

POST /api/v1/partners/v1/saju-flow

ParameterTypeRequiredDescription
birthDatestringOYYYY-MM-DD (1900~2100). A date in the calendar given
birthTimestring | nullHH:mm. If absent, the hour pillar is unknown — calculated on six characters, with hourKnown: false
calendarstringOsolar · lunar (lunar dates are converted to solar for the calculation)
isLeapMonthbooleanLunar leap month (default false)
genderstring | nullmale · female. If absent, there is no 10-year luck layer (daeun: [], daeunIndex: null)
timezonestringOnly Asia/Seoul is accepted
birthLatitude · birthLongitudenumberBirthplace coordinates (both must be sent). Same fields and rules as the Manseryeok API — the longitude is used for true solar time correction, and Fortune Flow is drawn on the same corrected natal chart Manseryeok returns. Sending only one returns 400 INVALID_LOCATION. If not sent, Manseryeok's default handling applies (currently no correction), reported in assumedLocation
countrystringCountry of birth, 2-letter code. Sending KR·JP·TH without coordinates corrects with that country's representative longitude (same as Manseryeok)
birthTimePrecisionstringexact · approximate · estimated · unknown. Correction is applied only for exact (the default is exact when a location is sent). Any value other than exact means no true solar time correction. Births during Korean daylight saving time periods are read as standard time by subtracting 1 hour, as in Manseryeok (2026-10-16)
rangeobjectO{ "fromYear", "toYear" }, the annual-luck range (solar years, up to 101 years). We recommend fetching birth year ~ birth year+100 once and slicing it on screen
monthsOfYearinteger | nullThe Saju year for which to calculate the 12 monthly-luck slots (from that year's Ipchun to just before the next year's Ipchun). If omitted, the Saju year containing now (Korea time) — from Jan 1 to before Ipchun, that is the previous year. null means no monthly luck layer (months: [])
langstringko · en · ja · th. Response values are language-independent codes (Korean stem-branch readings, Five Elements, relation names), so results are currently the same. For the full list, see Valid values > Language-independent code values.

Request example

{
  "birthDate": "1990-05-15",
  "birthTime": "14:30",
  "calendar": "solar",
  "gender": "male",
  "timezone": "Asia/Seoul",
  "range": { "fromYear": 1990, "toYear": 2090 },
  "monthsOfYear": 2026
}
curl -X POST https://api.hrum.ai/api/v1/partners/v1/saju-flow \
  -H "X-Partner-Api-Key: $HRUM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: chart-123:flow:2026" \
  -d '{"birthDate": "1990-05-15", "birthTime": "14:30", "calendar": "solar", "gender": "male", "timezone": "Asia/Seoul", "range": {"fromYear": 1990, "toYear": 2090}, "monthsOfYear": 2026}'

Response example (excerpt, actual engine output)

{
  "success": true,
  "data": {
    "engineVersion": "saju-flow-6",
    "kbVersion": "2026.10.11-7",
    "hourKnown": true,
    "natalAmbiguous": null,
    "natal": {
      "score": -37, "result": "중화", "level": "약약",
      "breakdown": { "season": -30, "support": 36, "control": -28, "hidden": -5, "root": 5 },
      "primaryYongshin": "토",
      "shin": { "yongshin": "토", "huishin": "화", "gishin": "목", "gushin": "수", "hanshin": "금" },
      "ruleIds": ["R-SAJU-STR-001", "R-SAJU-STR-018", "R-SAJU-STR-019", "R-SAJU-YONG-170"]
    },
    "daeun": [
      { "startYear": 1997, "endYear": 2006, "startAge": 7,
        "chars": [
          { "pos": "stem", "char": "임", "hanja": "壬", "ohaeng": "수", "sipsin": "식신", "shinRole": "gushin",
            "share": { "support": 0, "control": -8, "hidden": 0, "root": 0, "total": -8 } },
          { "pos": "branch", "char": "오", "hanja": "午", "ohaeng": "화", "sipsin": "정관", "shinRole": "huishin",
            "share": { "support": 0, "control": -10, "hidden": -5, "root": 0, "total": -15 } }
        ],
        "delta": -23, "line": { "value": -60, "clamped": false },
        "relations": [
          { "type": "자형", "from": "daeun", "with": "yearBranch", "a": "오", "b": "오", "ruleId": "R-SAJU-REL-044", "decisionRuleId": "R-SAJU-UN-036" },
          { "type": "육합", "from": "daeun", "with": "hourBranch", "a": "오", "b": "미", "ruleId": "R-SAJU-REL-015", "decisionRuleId": null }
        ],
        "triples": [], "hapPulim": [ ... ] },
      ...
    ],
    "years": [
      { "year": 2026, "daeunIndex": 2,
        "chars": [ { "pos": "stem", "char": "병", "hanja": "丙", ... "share": { ..., "total": -10 } },
                   { "pos": "branch", "char": "오", "hanja": "午", ... "share": { ..., "total": -15 } } ],
        "delta": -25, "line": { "value": -33, "clamped": false },
        "relations": [
          { "type": "천간합", "from": "seun", "with": "monthStem", "a": "병", "b": "신", "ruleId": "R-SAJU-REL-003", "decisionRuleId": null },
          ...
        ],
        "withDaeun": [], "triples": [],
        "hapPulim": [
          { "kind": "existing", "domain": "jiji", "pair": ["오", "미"], "status": "broken_by_dong",
            "reasonCode": "branch_hap_broken", "params": { "incoming": "오", "origin": "seun" },
            "triggeredBy": { "origin": "seun", "value": "오" },
            "reasonFallback": "동기 오(seun) 도래 → 합반 깨짐" },
          { "kind": "new", "domain": "cheongan", "pair": ["병", "신"], "chartValue": "신", "incomingValue": "병", "incomingOrigin": "seun",
            "reasonCode": "new_stem_hap", "params": { "incoming": "병", "origin": "seun", "partner": "신" },
            "reasonFallback": "병(seun) + 원국 신 → 새 천간 합 형성" },
          ...
        ],
        "ipchunNote": false },
      ...
    ],
    "months": [
      { "index": 1, "startsAt": "2026-02-04T04:55:00+09:00",
        "chars": [ { "pos": "stem", "char": "경", ... }, { "pos": "branch", "char": "인", ... } ],
        "delta": 4,
        "relations": [ { "type": "반합", "from": "wolun", "with": "yearBranch", "a": "인", "b": "오", "ruleId": "R-SAJU-REL-024", "decisionRuleId": "R-SAJU-UN-035" }, ... ] },
      ...
    ],
    "compatInputs": {
      "primaryYongshin": "토",
      "surfaceElementCounts": { "목": 0, "화": 2, "토": 2, "금": 3, "수": 1 },
      "surfaceAbsent": ["목"],
      "hourKnown": true,
      "ruleIds": ["R-SAJU-COMPAT-008", "R-SAJU-COMPAT-009", "R-SAJU-COMPAT-010"]
    },
    "caveats": ["R-SAJU-UN-028", "R-SAJU-PRAC-026", "R-SAJU-UN-034"],
    "birthBeforeIpchun": false,
    "assumedLocation": { "correction": "none" },
    "costPoints": 0
  }
}

Reading the fields

Pricing

Error codes

Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:

HTTPCodeWhen
404saju_flow_not_enabledThe feature is not enabled for the account
400INVALID_DATEbirthDate format error or a date that does not exist
400INVALID_LUNAR_DATEA lunar date that does not exist
400INVALID_RANGEtoYear is before fromYear, or the range exceeds 101 years
400INVALID_TIMEbirthTime is not in HH:mm format
400INVALID_LOCATIONOnly one of birthLatitude·birthLongitude sent
409saju_flow_correction_unsupportedA location was sent, but this is a rare birth where the corrected natal chart and Daeun starting number cannot be carried over to Fortune Flow exactly as in Manseryeok (about 0.1% in our measurements — e.g. a correction crossing midnight changes the Daeun starting number, or the noon correction for an unknown time crosses a solar-term start). It refuses rather than making up different values (not charged). For such births, use the Manseryeok API with the same location
402(none)Balance is less than the call price — this API has no pre-call balance check, so you get a 402 with no code

Compatibility API

Calculates the compatibility of two people from their Saju (Four Pillars of Destiny). In one call it returns Saju compatibility (60×60 day-pillar compatibility table · scores and six axes by purpose · Heavenly Stem / Earthly Branch relations · Five Elements balance across all eight characters · Ten Gods · cross Sinsal (symbolic stars)), the intimacy symbol table (symbols and grades from the male/female day-pillar table of a reference text (1995)), zodiac compatibility (three layers — Sun, Moon, Ascendant — plus planets chosen by purpose, and up to two zodiac compatibility paragraphs) and zodiac-animal compatibility (the Earthly Branch relation and grade of the two birth-year animals), plus an overall score (headerBreakdown) that combines five scores with purpose-specific weights. Since 2026-10-16 it also returns a relations table (relations) that collects, position by position, the combinations, clashes, punishments, harms and breaks between the two natal charts, and its score (p2Score). It is the same engine as the compatibility screen in the INTA app, and the same input always gives the same result.

The headline score in the INTA app (the big number) = headerBreakdown.average (62 in the response example below). saju.score.total (59 in the example) is HRUM's own score that averages only the day-pillar table and the eight characters, so it differs from the number on the app screen.

The values shown in the six-axis chart on the INTA app result screen are saju.iljuAxes (saju.axes is a different set, recalculated per purpose). For people whose birth time is known, send personXCountry + personXBirthTimePrecision: "exact" as in the recommended call so the Saju is calculated the same way as in the INTA app (with true solar time correction).

POST /api/v1/partners/v1/compat

Analyzes compatibility based on the Saju of two people.

Request parameters

Parameter Type Required Description
personABirthDate string O A's birth date (YYYY-MM-DD, solar calendar by default; lunar if personACalendar: "lunar")
personABirthTime string A's birth time (HH:MM, optional)
personAGender string A's gender ("male"/"female"; "남"/"여" (male/female) also accepted; optional)
personBBirthDate string O B's birth date (YYYY-MM-DD, solar calendar by default; lunar if personBCalendar: "lunar")
personBBirthTime string B's birth time (HH:MM, optional)
personBGender string B's gender ("male"/"female"; "남"/"여" (male/female) also accepted; optional)
purpose string Purpose of the compatibility — yeonae_attraction (dating, default) · marriage_stability (marriage) · friendship (friendship) · family_bond (family) · coworker_cooperation (coworkers) · business_partnership (business). Scores, six axes, relationship type, relationship narrative, priority planets in zodiac compatibility and the overall score weights all change with the purpose. Any other value returns 400 INVALID_PURPOSE (not charged)
lang string Text language — ko (default) · en · ja · th. The text in categoryNarrative · astroReadings · astroSynastry, the classic1995 grade and tti come in this language. In the saju block, the key, meetingKeywords, authority, full8 and conflict of relationRatings, plus dayFlow.summary, yearFlow.years[].summary and dictionary, also follow this language. Since 2026-10-13 the basic saju fields (highlights · relation · tenGods · elements · bestFor/weakFor · pattern.name) also come in this language. mbti and yearFlow.years[].a/b are Korean only for now
personABirthTimePrecision / personBBirthTimePrecision string Time precision — exact · approximate (to the two-hour period) · estimated · unknown. Zodiac compatibility: defaults to exact if a time is given, otherwise unknown. Saju: true solar time correction applies only with exact; if you omit it and send coordinates or personXCountry, it is treated as exact (same rule as the Manseryeok API)
personABirthLatitude / personABirthLongitude
personBBirthLatitude / personBBirthLongitude
number Birthplace coordinates (send both). Used for both the Ascendant calculation and the Saju true solar time correction (applied to Saju since 2026-10-11). Without them, the Ascendant is estimated from personXCountry, or failing that from a representative coordinate for the country of lang (ko → Korea, ja → Japan, th → Thailand / for en the Ascendant is omitted)
personABirthTimezoneOffset / personBBirthTimezoneOffset number UTC offset in minutes, default 540 (KST). When sent with coordinates, it also sets the standard meridian for the Saju correction (if omitted, the time zone closest to the longitude). If personXBirthTimezone is also sent, that takes priority
personABirthTimezone / personBBirthTimezone string Birthplace time zone (IANA, e.g. "America/New_York", optional, 2026-10-14). The Saju is calculated with the same rule as the Manseryeok API's timezone, so the same birth data gives the same four pillars as the Manseryeok API — outside Seoul, solar term entry times are read in that time zone (including daylight saving time), and when sent with coordinates, the standard offset without daylight saving time is used as the correction base. It takes priority over personXBirthTimezoneOffset, and the zodiac compatibility chart is drawn with the clock time actually in use in that time zone at the time of birth (including daylight saving time). If omitted, the calculation is the same as before (the birth time is read as Korean time). If the name is not recognized, it is calculated as if omitted and the response gets warnings (UNKNOWN_TIMEZONE, one per person)
personACalendar · personAIsLeapMonth
personBCalendar · personBIsLeapMonth
string · boolean With "lunar", the birth date is taken as a lunar date and converted to the solar calendar for the calculation (default "solar"). IsLeapMonth: true means a leap month; if that year has no such leap month or the lunar date does not exist, 400 INVALID_LUNAR_DATE (not charged). If either person was sent as lunar, the response gets lunarInput ({a, b}, same format as the Manseryeok API)
personACountry / personBCountry string Two-letter country code of birth. Sending KR · JP · TH without coordinates corrects the Saju time with that country's representative longitude (no correction for other countries). Zodiac compatibility also uses this country instead of lang to estimate the Ascendant
personAZaSiRule / personBZaSiRule string Day pillar for the Ja hour (after 23:00): "traditional" (default, next day's day pillar) · "yajaSi" (Yajasi, same day's day pillar · after location correction, the hour-pillar stem in the 23:00 hour is based on the next day's Day Master, since 2026-10-20)
details boolean Default false. If true, adds saju.full8 (detailed eight-character analysis) · saju.conflict ({pattern, tip}, conflict pattern and advice) · saju.relationScores (Heavenly Stem / Earthly Branch relation scores) · top-level dictionary ({a, b}, day-pillar dictionary for both people — basic values of the stem-branch pair (Five Elements · yin/yang · hidden stems · Twelve Life Stages), personality keywords, one-line summary). The id (ilju_경진) and name_kr in dictionary.a/b are language-independent day-pillar code values (Korean). For en and th, single stem/branch characters, Twelve Life Stages etc. are romanized (Gap · Mogyok); for ja they are in Chinese characters. The object keys of personA/B.elementDistribution · sipsinCount · combinedElementBalance.distribution in saju.full8 are language-independent Korean code values (목, 비견, etc.). For the full list see Valid values > language-independent code values.
flow boolean Default false. If true, adds yearFlow (the two people's flow over five years, base year ±2) and dayFlow (the two people's flow over 15 days, base date ±7)
date string Base date for flow, YYYY-MM-DD (1900-2100). Default: today in Korean time
currentYear number Base year for yearFlow (1900–2100). Default: the year of date
personAMbti / personBMbti string Four-letter MBTI (e.g. ENFP, case-insensitive). If you send both, a top-level mbti (MBTI compatibility, Korean) is added

The details, flow and MBTI options cost nothing extra (still ₩20 per call).

For people whose birth time is known, send the country of birth together with "exact". The true solar time correction then matches the INTA app, so even people born near a two-hour period boundary get the same day pillar and scores as in the app.

{
  "personABirthDate": "1990-09-04", "personABirthTime": "14:30", "personAGender": "male",
  "personACountry": "KR", "personABirthTimePrecision": "exact",
  "personBBirthDate": "1992-02-20", "personBBirthTime": "10:15", "personBGender": "female",
  "personBCountry": "KR", "personBBirthTimePrecision": "exact",
  "purpose": "yeonae_attraction", "details": true
}

Response example

Below is real engine output (A male 1990-09-04 14:30, B female 1992-02-20 10:15, dating, no coordinates). The always-included fields added on 2026-10-10 (saju.relationRatings · saju.meetingKeywords · saju.authority · tti.conflictFrame) are left out of this example — see the options example below. For these two people the values are the same even with the recommended call (KR + exact).

{
  "success": true,
  "data": {
    "personABirthDate": "1990-09-04", "personABirthTime": "14:30", "personAGender": "male",
    "personBBirthDate": "1992-02-20", "personBBirthTime": "10:15", "personBGender": "female",
    "tti": {
      "userTti": "말", "partnerTti": "원숭이",
      "relation": { "type": null, "detail": null, "relations": [] },
      "tier": "흉"
    },
    "saju": {
      "purpose": "yeonae_attraction",
      "hourKnown": { "a": true, "b": true },
      "personA": { "ilju": "임신" },
      "personB": { "ilju": "병인" },
      "score": { "total": 59, "grade": "맞춰 가면 좋아요",
                 "breakdown": { "ilju": 53, "authority": 65, "eightChar": 65 } },
      "axes": { "attraction": 25, "stability": 59, "growth": 58,
                "communication": 24, "passion": 74, "longevity": 65 },
      "pattern": { "name": "폭발형",
                   "description": "에너지가 크게 부딪히는 뜨거운 관계예요. 감정이 커질 때 잠시 쉬어 가는 규칙이 도움이 돼요." },
      "relation": { "summary": "천간충 + 인신충",
                    "stem": { "type": "천간충", "detail": "병임충" },
                    "branches": [ { "type": "지지충", "detail": "인신충" } ] },
      "elements": { "basis": "eight", "aLacks": [], "bProvidesToA": [], "bLacks": ["토"], "aProvidesToB": ["토"] },
      "tenGods": { "aView": "편재", "bView": "편관(칠살)" },
      "bestFor": ["단기 끌림"],
      "weakFor": ["결혼·동거"],
      "highlights": [ "상대의 시지에 나의 천을귀인 (사) 발현 — 상대가 나의 해결사" ],
      "iljuAxes": { "attraction": 45, "stability": 16, "growth": 88,
                    "communication": 60, "passion": 70, "longevity": 50 }
    },
    "categoryNarrative": {
      "category": "romantic", "purpose": "yeonae_attraction", "lang": "ko",
      "text": "상대 쪽의 즉각적인 열정과 나 쪽의 자유로운 흐름은 불과 물처럼 정면으로 맞부딪혀서, 처음 만났을 때의 팽팽한 긴장감이 곧 끌림이 되기 쉬워요. 짧고 뜨거운 설렘이 큰 관계라, 감정이 치솟는 날엔 하루쯤 식힐 틈을 두면 더 즐겁게 만날 수 있어요.",
      "styleVersion": "v2"
    },
    "costPoints": 20,
    "classic1995": {
      "symbol": "××", "grade": "대흉", "score": 50,
      "ttiRelation": { "type": null, "detail": null, "relations": [] },
      "conflictFrame": false
    },
    "relations": [
      { "positions": ["month", "month"], "kind": "chung", "tier": "heavyClash",
        "ruleIds": ["R-SAJU-REL-034", "R-SAJU-COMPAT-106", "R-SAJU-COMPAT-112"], "chars": ["申", "寅"] },
      { "positions": ["day", "day"], "kind": "chung", "tier": "heavyClash",
        "ruleIds": ["R-SAJU-REL-034", "R-SAJU-COMPAT-106", "R-SAJU-COMPAT-112"], "chars": ["申", "寅"] },
      { "positions": ["day", "day"], "kind": "stemChung", "tier": "clash",
        "ruleIds": ["R-SAJU-REL-008", "R-SAJU-COMPAT-110"], "chars": ["壬", "丙"] },
      { "positions": ["year", "year"], "kind": "napeumSaeng", "tier": "gather",
        "ruleIds": ["R-SAJU-BASE-037", "R-SAJU-COMPAT-111"], "chars": ["庚午", "壬申"] },
      { "positions": ["chart", "chart"], "kind": "banghap", "tier": "gather",
        "ruleIds": ["R-SAJU-REL-021", "R-SAJU-COMPAT-104"], "chars": ["巳", "午", "未"] }
    ],
    "p2Score": 57,
    "astroSynastry": {
      "self": {
        "sun": { "signEn": "Virgo", "element": "earth", "modality": "mutable" },
        "moon": { "signEn": "Pisces", "element": "water", "modality": "mutable", "uncertain": false },
        "ascendant": { "signEn": "Sagittarius", "element": "fire", "modality": "mutable", "uncertain": false, "countryBasis": true },
        "venus": { "signEn": "Leo", ... }, "mars": { "signEn": "Gemini", ... },
        "mercury": { ... }, "jupiter": { ... }, "saturn": { ... }
      },
      "partner": { "sun": { "signEn": "Pisces", ... }, "moon": { "signEn": "Virgo", ... }, ... },
      "layers": [
        { "key": "sun", "relation": "opposite", "selfSign": "Virgo", "partnerSign": "Pisces",
          "textKey": "compat.astro.relation.opposite",
          "text": "충 — 정반대라 방향은 다르지만 서로를 끌어당길 수 있어요" },
        { "key": "moon", "relation": "opposite", "selfSign": "Pisces", "partnerSign": "Virgo",
          "textKey": "compat.astro.relation.opposite",
          "text": "충 — 정반대라 방향은 다르지만 서로를 끌어당길 수 있어요" },
        { "key": "ascendant", "relation": "neutral", "selfSign": "Sagittarius", "partnerSign": "Taurus",
          "textKey": "compat.astro.relation.neutral",
          "text": "뚜렷한 결 없이 각자의 스타일을 존중하면 되는 조합이에요" }
      ],
      "purposeBodies": [
        { "body": "venus", "relation": "opposite", "selfSign": "Leo", "partnerSign": "Aquarius",
          "textKey": "compat.astro.relation.opposite",
          "text": "충 — 정반대라 방향은 다르지만 서로를 끌어당길 수 있어요" },
        { "body": "mars", "relation": "same_element", "selfSign": "Gemini", "partnerSign": "Aquarius",
          "textKey": "compat.astro.relation.same_element",
          "text": "같은 원소 — 서로의 방식을 바로 알아볼 만큼 편안할 수 있어요" }
      ],
      "caveats": ["compat.astro.caveat.ascendantCountryBasis"],
      "caveatTexts": ["상승궁은 출생 도시가 없어 국가 대표 좌표로 추정했어요"],
      "score": 64
    },
    "astroReadings": [
      { "key": "Su_Mo_ss", "lang": "ko",
        "text": "한쪽의 태양과 다른 쪽의 달이 같은 별자리에 놓이면, 한 사람이 향하는 방향을 다른 사람의 마음이 낯설지 않게 알아보는 깊은 공명이 생기기 쉬워요. 닮은 만큼 같은 그림자도 함께 커질 수 있으니 서로의 빈틈을 웃으며 짚어 주면 좋아요." },
      { "key": "Ve_Ma_op", "lang": "ko",
        "text": "한쪽의 금성과 다른 쪽의 화성이 정반대 별자리에 놓이면, 서로에게 없는 매력을 알아보며 끌리는 보완의 흐름이 생기기 쉬워요. 원하는 속도와 표현 방식이 반대로 기울 수 있으니 서로의 선을 말로 정해 두면 그 끌림이 편안하게 이어져요." }
    ],
    "headerBreakdown": {
      "sixAxis": 55, "authority": 65, "full8": 65, "classic": 57, "natal": 64,
      "average": 62, "purpose": "yeonae_attraction",
      "weights": { "sixAxis": 0.2, "authority": 0.3, "full8": 0.2, "classic": 0.1, "natal": 0.2 }
    },
    "engineVersion": "compat-2",
    "lang": "ko"
  }
}

Relation kinds (relations[].kind)

kindMeaningtier
yukhapBranch six harmony — two branches pair up and combinegather
banhapBranch half harmony — two of the three characters of a three-harmony setlistOnly
samhapThree harmony — combining the two charts brings three characters together to form one element framegather
banghapDirectional combination — combining the two charts brings together three branches of the same seasongather
chungBranch clash — head-on collisionheavyClash
hyungBranch punishmentclash
haeBranch harmclash
paBranch break (the 寅亥 · 巳申 pairs are six harmonies, so listOnly)clash
wonjinWonjin (resentment)listOnly
stemHapStem combination between the two Day Mastersgather
stemChungStem clash between the two Day Mastersclash
napeumSaengThe Nayin elements of the year pillars generate each othergather
napeumBihwaThe Nayin elements of the year pillars are the samegather
napeumGeukThe Nayin elements of the year pillars control each otherclash

The same kind always has the same tier (the only exception is the two break pairs). New kind values may be added, so skip values you don't know.

Options example (details · flow · MBTI)

An excerpt of real engine output. Request:

{
  "personABirthDate": "1990-05-15", "personABirthTime": "12:00", "personAGender": "male",
  "personBBirthDate": "1992-08-20", "personBBirthTime": "09:00", "personBGender": "female",
  "details": true, "flow": true, "date": "2026-10-10",
  "personAMbti": "ENFP", "personBMbti": "ISTJ"
}
{
  "success": true,
  "data": {
    "tti": { "userTti": "말", "partnerTti": "원숭이", ..., "tier": "흉", "conflictFrame": false },
    "saju": {
      "personA": { "ilju": "경진" }, "personB": { "ilju": "무진" },
      "score": { "total": 56, "grade": "맞춰 가면 좋아요", ... },
      ...,
      "relationRatings": [
        { "key": "연애", "stars": 3 }, { "key": "결혼·동거", "stars": 3 }, { "key": "친구", "stars": 4 },
        { "key": "사업 파트너", "stars": 3 }, { "key": "장기 관계", "stars": 3 }
      ],
      "meetingKeywords": ["괴강", "우직", "일반적 관계"],
      "authority": {
        "finalScore": 51, "grade": "주의", "gradeTier": "caution",
        "pattern": "자형 (자기 갈등 외부화)",
        "summary": "주의 · 자형 (자기 갈등 외부화) · 익숙한 결의 만남. 안락함 위에 자극을 어떻게 만들지가 핵심.",
        "levels": { "L1": { "raw": 0, "norm": 0 }, "L2": { "raw": 5, "norm": 14.3, "type": "동계절 (봄)" },
                    "L3": { "raw": 17, "norm": 56.7 }, "L4": { "raw": -17, "norm": -42.5 } },
        "factors": { "l1": [], "l2Tone": "...", "l3": [ ... ], "l4": ["자형(진진) — 자기 파괴 패턴", ...] },
        "flags": { "cheonHapJiHap": false, "dualChung": false },
        "climateBalance": "동계절 (봄)"
      },
      "conflict": {
        "pattern": "경은 무를 편인으로 인식. 일방향적 관계 성격.",
        "tip": "'괴강'과 '우직'은 서로 다른 결이에요. 같은 사건도 다르게 해석한다는 걸 인정하고, 서로의 관점을 한 번씩 빌려보세요."
      },
      "relationScores": { "stem": { "type": "편인", "category": "neutral", "score": 55 },
                          "branches": [ { "type": "자형", "score": -40 } ] },
      "full8": { "personA": { ... }, "personB": { ... }, "iljuEvaluation": { ... }, ... }
    },
    ...,
    "dictionary": { "a": { "id": "ilju_경진", "name_kr": "경진", "name_hanja": "庚辰", ... }, "b": { ... } },
    "yearFlow": {
      "baseYear": 2026,
      "years": [
        { "year": 2024, "score": 77, "tier": "great", "a": { ... }, "b": { ... },
          "summary": "두 분 모두 흐름이 좋아 같이 시작하기 좋은 해." },
        ...
      ]
    },
    "dayFlow": {
      "baseDate": "2026-10-10",
      "days": [
        { "date": "2026-10-03", "a": -89, "b": -40, "combined": -60, "tier": "hard", "isBaseDate": false },
        ...,
        { "date": "2026-10-10", "a": -9, "b": 27, "combined": 9, "tier": "neutral", "isBaseDate": true },
        ...
      ],
      "golden": [],
      "hard": [ { "date": "2026-10-03", "combined": -60 }, ... ],
      "summary": "3번 같이 가라앉는 날이 있어. 그날은 큰 결정·약속 미루자."
    },
    "mbti": { "id": "mbti_ENFP_ISTJ", "mbti_a": "ENFP", "mbti_b": "ISTJ",
              "socionics_relation": "Duality(이중관계)", "scores": { ..., "total": 64 }, ... }
  }
}

Notes

Error codes

Besides the common errors (401 · 400 body validation · 402 · 429 · 503, etc.), this API returns:

HTTPCodeWhen
400INVALID_BIRTH_YEAROne person's birth year is outside 1900–2100
400INVALID_LUNAR_DATEThe lunar date does not exist
400COMPAT_UNAVAILABLENo compatibility data for this day-pillar combination
402INSUFFICIENT_BALANCEInsufficient balance

For request examples see the options example above and the run example.

Personality Type API

Provides an MBTI-based personality test. Supports a free lite version (24 questions) and a premium version (93 questions).

GET /api/v1/partners/v1/personality-type/questions

Gets the personality test questions. Free.

curl "https://api.hrum.ai/api/v1/partners/v1/personality-type/questions?tier=free" \
  -H "X-Partner-Api-Key: your-api-key"

Request parameters

Parameter Type Description
tier string Test version ("free" or "premium", default: "free")

Response example

{
  "success": true,
  "data": {
    "tier": "free",
    "count": 24,
    "questions": [
      {
        "id": "q_001",
        "text": "갑자기 생긴 모임에도 일단 가본다",
        "choices": [
          { "value": 1, "label": "전혀 그렇지 않다" },
          { "value": 2, "label": "그렇지 않다" },
          { "value": 3, "label": "보통이다" },
          { "value": 4, "label": "그렇다" },
          { "value": 5, "label": "매우 그렇다" }
        ]
      }
    ]
  }
}

POST /api/v1/partners/v1/personality-type

Submits personality test answers to analyze the personality type.

curl -X POST https://api.hrum.ai/api/v1/partners/v1/personality-type \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "tier": "free",
    "answers": [
      { "questionId": "q_001", "answer": 4 },
      { "questionId": "q_002", "answer": 2 }
    ]
  }'

Request parameters

Parameter Type Required Description
answers array O Answer array [{ "questionId": "q_001", "answer": 4 }, ...]. questionId is the id from the questions response (format q_001); answer is an integer 1–5. premium needs at least one answer on each of the four axes (extraversion/introversion · sensing/intuition · thinking/feeling · judging/perceiving) (otherwise 400 personality_axis_unanswered). For accurate results we recommend answering all 93 questions you get from the questions endpoint
tier string O "free" or "premium"

Response example

{
  "success": true,
  "data": {
    "personalityType": "ENTJ",
    "typeLabel": "지휘관",
    "scores": {
      "extraversion": 72,
      "sensing": 45,
      "thinking": 68,
      "judging": 80
    },
    "tier": "premium",
    "costPoints": 10
  }
}

Pricing

Error codes

Besides the common errors (401 · 400 body validation · 402 · 429 · 503, etc.), this API returns:

HTTPCodeWhen
400(none)Questions — tier is not free or premium / Submit — unknown questionId
400personality_axis_unansweredSubmit — premium, and an axis has no answers at all (empty axes in axes)
402INSUFFICIENT_BALANCESubmit — premium, and the balance is insufficient

An answer outside 1–5 returns the common 400. None of these are charged.

Interpretation KB API

This API is open only to accounts that have applied for it. Other accounts get 404 kb_saju_explain_not_enabled on all three paths (not charged). To use it, contact support.

Explains the relations (combinations, clashes, etc.) and structures (Ten Gods, hidden stems, etc.) that hold in a Saju natal chart as rule cards and explanatory sentences. It does not use an LLM, so the same input always gives the same result.

POST /api/v1/partners/v1/kb/saju/explain

Request parameters

ParameterTypeRequiredDescription
birthobjectOSame fields as the Manseryeok API request body (year · month · day · hour? · minute? · timezone · gender · calendar? …). birth.lang is ignored. If birth.timezone is not recognized, the response gets warnings (field: "birth.timezone")
sectionsstring[]relations · structure · all. If omitted, everything (in the order relations → structure)
levelstringL1 (default) · L3. Items without an L3 explanation are filled with L1 and get fallbackLevel
langstringOnly ko is supported
channelstringstable (default, reviewed explanations only) · preview (includes explanations not yet reviewed — only for specially allowed accounts, otherwise 403 KB_PREVIEW_NOT_ALLOWED)

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/kb/saju/explain \
  -H "X-Partner-Api-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "birth": {
      "year": 1990, "month": 5, "day": 17, "hour": 14, "minute": 30,
      "timezone": "Asia/Seoul", "gender": "female"
    },
    "sections": ["relations"]
  }'

Response example (excerpt)

{
  "success": true,
  "data": {
    "kbVersion": "2026.10.09-3",
    "kbHash": "sha256:24b9a2d9...",
    "channel": "stable",
    "chart": {
      "pillars": {
        "year":  { "stem": "庚", "branch": "午" },
        "month": { "stem": "辛", "branch": "巳" },
        "day":   { "stem": "壬", "branch": "午" },
        "hour":  { "stem": "丁", "branch": "未" }
      },
      "timeUnknown": false
    },
    "sections": [
      {
        "id": "relations",
        "title": "...",
        "steps": [
          {
            "fact": { "type": "stem_pair", "a": "壬", "b": "丁", "posA": "day", "posB": "hour", "relation": "hap" },
            "factText": "일간 壬과 시간 丁은 천간합(天干合) 관계다.",
            "componentId": "saju:stem_pair:丁壬:hap",
            "rules": [
              { "id": "R-SAJU-REL-004", "kind": "definition", "status": "verified", "sourceCount": 2,
                "statement": "丁과 壬은 천간합 관계이고, 이 짝의 합화 오행은 목(木)이다." }
            ],
            "interp": {
              "meaning": { "text": "丁(정)과 壬(임)은 천간합으로 묶이는 짝이고, ...", "reviewed": false },
              "why": { ... },
              "pitfall": { ... }
            }
          }
        ],
        "absent": [
          { "type": "stem_pair", "a": "庚", "b": "辛", "posA": "year", "posB": "month",
            "note": "庚과 辛은 천간합·천간충 어느 것도 아니다." }
        ]
      }
    ],
    "unregistered": [],
    "pendingReview": []
  }
}

GET /api/v1/partners/v1/kb/rules/{id}

Returns one rule card and its original sources. Free.

curl https://api.hrum.ai/api/v1/partners/v1/kb/rules/R-SAJU-REL-004 \
  -H "X-Partner-Api-Key: your-api-key"
{
  "success": true,
  "data": {
    "id": "R-SAJU-REL-004",
    "kind": "definition",
    "statement": "丁과 壬은 천간합 관계이고, 이 짝의 합화 오행은 목(木)이다.",
    "status": "verified",
    "schools": [],
    "variantOf": null,
    "sources": [
      { "sourceId": "SRC-SMTH", "location": "『三命通會』 卷二 論十干化氣",
        "quote": "故丁與壬合 丁壬化木", "match": "EXACT", ... }
    ],
    "engineRef": "..."
  }
}

GET /api/v1/partners/v1/kb/release?domain=saju

Returns the currently published edition of the Interpretation KB. Free. If domain is omitted it is saju (e.g. saju · ziwei · astro · dream · tarot).

curl "https://api.hrum.ai/api/v1/partners/v1/kb/release?domain=saju" \
  -H "X-Partner-Api-Key: your-api-key"
{
  "success": true,
  "data": {
    "version": "2026.10.09-3",
    "hash": "sha256:24b9a2d9...",
    "publishedAt": "2026-10-09T00:00:00.000Z",
    "channel": "stable",
    "counts": { "rules": 412, "interps": 1236 }
  }
}

The numbers above are examples. If you store results, fetch them again when hash changes.

Error codes

Besides the common errors:

HTTPCodeWhen
404kb_saju_explain_not_enabledAccount has not applied (all three paths)
400INVALID_LUNAR_DATE · INVALID_LOCATIONexplain — the lunar date does not exist / only one of latitude and longitude was sent
403KB_PREVIEW_NOT_ALLOWEDexplain — account not allowed to use channel: "preview" (not charged)
400KB_DOMAIN_UNKNOWNrelease — unsupported domain
404KB_RULE_NOT_FOUNDrules — unknown rule id
503KB_NOT_LOADEDThe Interpretation KB edition has not been loaded yet. Try again shortly

explain charges only successful calls; failed calls are recorded as ₩0.

App Email API

An API that lets apps hosted on RIDM Apps send transactional and service emails such as sign-up confirmations, order notices and password resets. It can only be called with the app key (HRUM_API_KEY, console > My apps > AI connection); calling it with a regular API key returns 403 MAIL_APP_KEY_REQUIRED. SMTP ports are blocked in app containers, so please send email through this API.

POST /api/v1/partners/v1/mail/send

Request parameters

ParameterTypeRequiredDescription
tostring | string[]O1–10 recipient addresses. Addresses with a display name (Name <address>) are not accepted
subjectstringOSubject, 1–200 characters, no line breaks
textstring△Body (plain text). Either text or html is required. Up to 100,000 characters
htmlstring△Body (HTML), up to 200,000 characters. Cleaned before sending: script · style · iframe · forms · event attributes are removed, only http(s) · mailto · tel links and only https images are kept. If there is no text, a plain-text body is generated from the HTML and sent along
replyTostringReply-to address. If absent, the address set in the console; if that is also absent, sent without a reply-to address
attachmentsobject[]Up to 3 attachments, 512KB in total. { "filename", "contentType", "content" } (content is base64). Only PDF · PNG · JPEG · GIF · WebP · TXT · CSV · ICS are accepted
kindstringtransactional (default) · notification. With notification, one-click unsubscribe headers are added and addresses that unsubscribed from that app are skipped (suppressed)

Request example

curl -X POST https://api.hrum.ai/api/v1/partners/v1/mail/send \
  -H "Content-Type: application/json" \
  -H "X-Partner-Api-Key: $HRUM_API_KEY" \
  -d '{
    "to": "user@example.com",
    "subject": "Welcome aboard",
    "text": "Your sign-up is complete. Get started with the link below.",
    "html": "<p>Your sign-up is complete.</p><p><a href=\"https://myapp.ridm.ai\">Get started</a></p>"
  }'

Response example

{
  "success": true,
  "data": {
    "requestId": "7f1c2a3e-…",
    "results": [ { "to": "user@example.com", "status": "sent", "messageId": "<…>" } ],
    "sent": 1,
    "chargedWon": 0
  }
}

status is one of sent (accepted by the mail server) · failed (could not be sent — not charged) · suppressed (skipped because the address bounced or reported spam, or unsubscribed from that app's notification emails).

Limits and pricing

ItemDetails
Monthly included emailsIncluded in your plan (Free 100 · Basic 1,000 · Pro 3,000, per app, reset on the 1st of each month, Korean time). The included emails cost nothing extra
Beyond the included emailsOn the free plan, sending stops until the 1st of next month. On paid plans, sending continues at 1 won (KRW) per email (excl. VAT; 1.1 won incl. VAT) only if you turn on extra sending in the console. Only emails accepted by the mail server are deducted from your credits, right after sending
Daily limit500 per app by default (1–5,000 in the console). Resets at midnight Korean time
Per-minute limit30 per minute per app

Errors

HTTPcodeDescription
400MAIL_INVALIDInvalid request. reason holds the reason (invalid_recipient, too_many_recipients, subject_invalid, advertising_not_allowed, body_required, attachment_type_not_allowed, attachments_too_large, etc.)
402insufficient_creditNot enough credits to pay for extra sending. Nothing is sent
403MAIL_APP_KEY_REQUIREDCalled with a key that is not an app key
403APP_NOT_ACTIVEThe app linked to the key is not running
402MAIL_OVERAGE_UNPAIDUnpaid extra email charges exceed the balance. Top up and the outstanding charges are deducted and sending reopens (sending within the included emails keeps working)
403MAIL_EMAIL_UNVERIFIEDNo email-verified console account
403MAIL_SUSPENDEDMail sending for this app is suspended because spam report or bounce thresholds were exceeded, or an operator stopped it
429MAIL_RATE_LIMITEDPer-minute limit exceeded. Send again after retryAfterSeconds
429MAIL_DAILY_CAP_EXCEEDEDDaily limit exceeded. Reopens at midnight Korean time
429MAIL_MONTHLY_LIMITThis month's included emails are used up (free plan, or extra sending is off)
503MAIL_UNAVAILABLEThe mail server is unavailable. Try again shortly

Add a live visitor count to a web metaverse

A free feature that shows "N people online now" in a web 3D space built with Three.js or on an ordinary web page. Register your site in console > Visitor count to get a public site key (pk_presence_…) and a one-line script. It is called straight from the browser, with no key or login.

Add the script

Put the one-line script from console > Visitor count on your page, and add data-hrum-online where the number should go.

<script src="https://hrum.ai/p/presence.js" data-key="pk_presence_YOUR_SITE_KEY" async></script>

<span data-hrum-online></span> online now

That's it. Each tab that opens the page sends a signal right away and then every 30 seconds, and puts the returned visitor count into every element with data-hrum-online. It stays empty until a number arrives.

Use the number yourself

The script creates window.HrumPresence. Use it to draw the number in your own UI, such as a 3D sign or badge.

HrumPresence.onChange(function (online) {
  // online: current visitor count (integer)
  myScene.setVisitorCount(online);
});

HrumPresence.online;    // last number received, null if none yet
HrumPresence.refresh(); // send once more now and update the number
HrumPresence.stop();    // stop counting and send a leave signal

onChange is called only when the number changes. If you register it after a number has already arrived, it is called once right away. If you added the script with async, HrumPresence may not exist yet, so wait until it appears and then register.

function whenPresence(fn) {
  if (window.HrumPresence) return fn(window.HrumPresence);
  setTimeout(function () { whenPresence(fn); }, 200);
}
whenPresence(function (p) { p.onChange(function (n) { myScene.setVisitorCount(n); }); });

Three.js example

hrum.ai/samples/presence-three/?key=pk_presence_… — an example that adds a "○ online now" badge and a 3D sign to a small room built with Three.js. The sign and the figures around the table are drawn from the number received through onChange. Take the page source as is and use it as a starting point. When you put it on your own site, use a key whose allowed origins include that site's address.

Read just the number outside a page

If you only need the number outside a page, for a dashboard or a bot, use the lookup API. It sends no signal, so it does not add to the visitor count.

curl https://api.hrum.ai/api/v1/partners/v1/presence/sites/pk_presence_…/online
# {"success":true,"data":{"online":42,"source":"beacon","updatedAt":"2026-10-31T01:23:45.000Z"}}

If no number appears, check that the current page's address (origin) is among the allowed origins. Allowed origins accept https only, and the port must match too.

Endpoints

Method · pathDescription
POST /api/v1/partners/v1/presence/beatSend a signal. Body { "siteKey", "sid" } (JSON, text/plain or application/json, up to 1KB). sid is a random value created per tab (16–64 characters: letters, digits, _, -). 403 if the Origin is not an allowed origin. Response { online }
POST /api/v1/partners/v1/presence/leaveRemoves the tab right away when it closes (navigator.sendBeacon). Body is the same as beat; response is 204
GET /api/v1/partners/v1/presence/sites/{siteKey}/onlineGet the current visitor count { online, source, updatedAt }. source is server (value sent by your server) or beacon (aggregated script signals). Callable from any page (CORS *) and cached for 10 seconds. 404 for revoked keys
POST /api/v1/partners/v1/presence/sites/{siteKey}/countSend the number from your server (below). Requires the server key, once per minute. You can also send per-room counts rooms
GET /api/v1/partners/v1/presence/sites/{siteKey}/roomsPer-room counts a room server uses to choose which room to join (below). Requires the server key, not public
GET /api/v1/partners/v1/presence/directoryList of sites that agreed to Metamoa sharing (name · address · visitor count). Cached for 30 seconds

Response examples

// POST /presence/beat
{ "success": true, "data": { "online": 12 } }

// GET /presence/sites/pk_presence_…/online
{ "success": true, "data": { "online": 12, "source": "beacon", "updatedAt": "2026-10-31T09:00:00.000Z" } }

// GET /presence/directory
{ "success": true, "data": [ { "label": "우주 광장", "siteUrl": "https://example.com/world", "online": 12 } ] }

Send the number from your server

If your own server (game server, realtime server, etc.) already knows the visitor count, it can send the number directly. Issue a server key (sk_presence_…) per site in console > Visitor count. The server key is shown only once when issued, so store it in a server environment variable and never put it in page or browser code. Reissuing it stops the previous server key immediately.

Per-room counts (optional)

If your server splits people into rooms (channels, instances), you can send per-room counts and capacities in the same count body. The room server reads them back with GET …/rooms when choosing which room to put a new person in. Per-room numbers can be read only with the server key and do not appear in the public lookups (online · directory) or in presence.js. Keep sending the total visitor count separately in online as now (it is not calculated from the room totals).

POST /api/v1/partners/v1/presence/sites/pk_presence_…/count
X-Presence-Secret: sk_presence_…
Content-Type: application/json

{
  "online": 98,
  "rooms": [
    { "id": "studio-1", "n": 30, "cap": 30 },
    { "id": "studio-2", "n": 28, "cap": 30 },
    { "id": "studio-3", "n": 40, "cap": 50 }
  ]
}

// 200
{ "success": true, "data": { "online": 98, "reportedAt": "2026-10-31T09:00:00.000Z" } }
GET /api/v1/partners/v1/presence/sites/pk_presence_…/rooms
Authorization: Bearer sk_presence_…

// 200
{ "success": true, "data": {
  "rooms": [
    { "id": "studio-1", "n": 30, "cap": 30, "free": 0 },
    { "id": "studio-2", "n": 28, "cap": 30, "free": 2 },
    { "id": "studio-3", "n": 40, "cap": 50, "free": 10 }
  ],
  "reportedAt": "2026-10-31T09:00:00.000Z"
} }

Keep the server key on your server only. If sk_presence_… ends up in browser code, an app or a public repository, anyone can change your visitor count. Put it in an environment variable or a secret store, and if it leaks, reissue it right away in console > Visitor count.

Please send once a minute. If no new value arrives for 5 minutes, it falls back to aggregated script signals. You can use it together with the browser script; the page badge then shows the number your server sent.

curl

curl -X POST "https://api.hrum.ai/api/v1/partners/v1/presence/sites/pk_presence_…/count" \
  -H "Content-Type: application/json" \
  -H "X-Presence-Secret: $HRUM_PRESENCE_SECRET" \
  -d '{"online": 128}'
# {"success":true,"data":{"online":128,"reportedAt":"2026-10-31T01:23:45.000Z"}}

Node (WebSocket server)

// Node 18+ (built-in fetch), next to your `ws` server
setInterval(() => {
  fetch("https://api.hrum.ai/api/v1/partners/v1/presence/sites/pk_presence_…/count", {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Presence-Secret": process.env.HRUM_PRESENCE_SECRET },
    body: JSON.stringify({ online: wss.clients.size }),
  }).catch(() => {}); // a missed report is fine: the next one comes in 60 s
}, 60_000);

Cloudflare Workers (Durable Object)

// One Durable Object per room holds the WebSockets and reports their number every 60 s.
// wrangler secret put HRUM_PRESENCE_SECRET ; vars: HRUM_SITE_KEY = "pk_presence_…"
import { DurableObject } from "cloudflare:workers";

const API = "https://api.hrum.ai/api/v1/partners/v1/presence";

export class Room extends DurableObject {
  async fetch(request) {
    const [client, server] = Object.values(new WebSocketPair());
    this.ctx.acceptWebSocket(server);
    if (!(await this.ctx.storage.getAlarm())) await this.ctx.storage.setAlarm(Date.now() + 60_000);
    return new Response(null, { status: 101, webSocket: client });
  }

  async alarm() {
    const online = this.ctx.getWebSockets().length;
    await fetch(`${API}/sites/${this.env.HRUM_SITE_KEY}/count`, {
      method: "POST",
      headers: { "Content-Type": "application/json", "X-Presence-Secret": this.env.HRUM_PRESENCE_SECRET },
      body: JSON.stringify({ online }),
    }).catch(() => {});
    // keep reporting while anyone is connected; after the last one leaves, 0 was the final report
    if (online > 0) await this.ctx.storage.setAlarm(Date.now() + 60_000);
  }
}

Supabase (Realtime Presence)

An Edge Function joins the channel your users track(), counts the presence keys and sends the number, and pg_cron calls it every minute. There is one key per connection, so to count each person once, create the channel on the user side with config: { presence: { key: userId } }.

// supabase/functions/presence-count/index.ts
// supabase secrets set HRUM_PRESENCE_SECRET=sk_presence_… HRUM_SITE_KEY=pk_presence_…
import { createClient } from "jsr:@supabase/supabase-js@2";

const API = "https://api.hrum.ai/api/v1/partners/v1/presence";

Deno.serve(async () => {
  const supabase = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!);
  const channel = supabase.channel("room-1"); // the channel name your clients use
  const online = await new Promise<number>((resolve) => {
    const count = () => resolve(Object.keys(channel.presenceState()).length);
    channel.on("presence", { event: "sync" }, count).subscribe();
    setTimeout(count, 5_000); // no sync within 5 s: report what is there
  });
  await supabase.removeChannel(channel);
  const res = await fetch(`${API}/sites/${Deno.env.get("HRUM_SITE_KEY")}/count`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Presence-Secret": Deno.env.get("HRUM_PRESENCE_SECRET")! },
    body: JSON.stringify({ online }),
  });
  return new Response(null, { status: res.ok ? 204 : 502 });
});
-- every minute (pg_cron + pg_net); project_url and service_role_key stored in Vault
select cron.schedule('hrum-presence-count', '* * * * *', $$
  select net.http_post(
    url := (select decrypted_secret from vault.decrypted_secrets where name = 'project_url') || '/functions/v1/presence-count',
    headers := jsonb_build_object('Authorization', 'Bearer ' ||
      (select decrypted_secret from vault.decrypted_secrets where name = 'service_role_key'))
  );
$$);

Metamoa sharing

If you turn on "Show in the Metamoa directory" in the console, the site's name, address and current visitor count appear in directories run by HRUM, such as Metamoa. If the site address does not match the allowed origins, it is not shown. You can turn it off at any time.

Privacy

Errors

HTTPcodeDescription
400PRESENCE_BAD_REQUEST · INVALID_BODYInvalid siteKey or sid format, or the body is not JSON
401PRESENCE_SECRET_REQUIREDServer key missing or wrong (including sending the public key) — count only
403PRESENCE_ORIGIN_NOT_ALLOWEDOrigin missing or not an allowed origin
404PRESENCE_SITE_NOT_FOUNDUnknown or revoked key
413PAYLOAD_TOO_LARGEBody exceeds 1KB
429PRESENCE_RATE_LIMITEDToo many signals, or count sent again within 1 minute (Retry-After). Send again shortly

Authentication API

Authentication API for managing console accounts.

POST /api/v1/partners/v1/console/auth/login

Logs in with email and password. New sign-ups with social accounts ("provider": "google" · "apple", POST …/console/auth/google · /apple) are no longer accepted since 2026-10-17 and return 410 SOCIAL_SIGNUP_DISABLED. If you already signed up with a social account, you can log in as before.

Request (email)

curl -X POST https://api.hrum.ai/api/v1/partners/v1/console/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "email",
    "email": "[email protected]",
    "password": "your-password"
  }'

Response

{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user": {
      "id": "user-123",
      "email": "[email protected]",
      "displayName": "John Doe",
      "provider": "email"
    }
  }
}

user.provider is one of email · apple · google.

POST /api/v1/partners/v1/console/auth/register

Registers a new account.

Request

curl -X POST https://api.hrum.ai/api/v1/partners/v1/console/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "secure-password",
    "displayName": "John Doe",
    "agreements": { "terms": true, "privacy": true }
  }'

password is 8–128 characters and displayName (display name or company name) is 2–50 characters. agreements.terms and agreements.privacy must be true (400 if missing or false), and agreements.aiTraining (boolean) is optional. agreements.marketing (boolean, optional) is consent to receive emails about new features and offers. If omitted or false, it is recorded as not consented.

Response

{
  "success": true,
  "data": {
    "message": "인증 메일을 발송했어요"  // "Verification email sent"
  }
}

POST /api/v1/partners/v1/console/auth/verify-email

Completes email verification. Send the token from the verification email together with the password set at sign-up.

Request

curl -X POST https://api.hrum.ai/api/v1/partners/v1/console/auth/verify-email \
  -H "Content-Type: application/json" \
  -d '{
    "token": "token-from-verification-email",
    "password": "secure-password"
  }'

Response

{
  "success": true,
  "data": {
    "message": "이메일 인증이 완료되었어요"  // "Email verification complete"
  }
}

If the token is wrong or expired, or the password does not match, you get 400 EMAIL_VERIFY_FAILED. The link in the verification email (GET /api/v1/partners/v1/console/auth/verify-email?token=…) does not verify anything; it only redirects with 302 to https://console.hrum.ai/verify-email.html?token=… — entering the password on that page calls the POST above.

Manage the console with AI

AI coding tools such as Claude Code, Cursor and Codex can handle console tasks for you. The AI can look things up right away, but anything that changes state — issuing keys, top-up links, hosting applications — goes ahead only after a person presses "Agree" on the console consent screen. The server rejects writes without a consent code, so there is no path that skips human consent. The one exception is hosting_ops below, which is open only on tokens where a person checked it at issue time.

Console management token

  1. The AI calls POST /api/v1/partners/v1/console-ai/consents with what it wants to do (what · amount · target) and gets a consentUrl and a code.
  2. The AI shows the link to the user (MCP can also open it in the browser).
  3. When the user opens the link while logged in to the console, the request is shown in full (amount · target · name) with a "Requested by AI" label. The user checks it and presses "Agree". Payment itself is then also made by the person in the Toss payment window.
  4. The AI checks the approval with GET /console-ai/consents/{code}, then continues by putting consentCode in the body of the write request.

A consent code can be used only once and expires after 10 minutes. A hash of the request is fixed into the code, so if the amount or target is changed after consent, the request is rejected (CONSENT_MISMATCH).

For the first token there is no token yet, so request it without authentication using {"action":"token_issue","payload":{"scopes":["keys"]}}. Put the pollSecret from the response into the X-Consent-Poll-Secret header to check the status; the token value is returned once, only on the first check after approval. account.email in the same response is the masked email of the console account that approved it. If it is not your account, revoke that token in the console.

curl -X POST https://api.hrum.ai/api/v1/partners/v1/console-ai/consents \
  -H "Authorization: Bearer $HRUM_CONSOLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"action":"credit_topup_link","payload":{"planKey":"credit_50000"}}'

# response data
{
  "code": "cc_…",
  "consentUrl": "https://console.hrum.ai/console-consent.html#c=cc_…",
  "expiresAt": "2026-10-16T06:10:00.000Z",
  "action": "credit_topup_link",
  "display": [
    { "label": "충전 상품", "value": "credit_50000" },
    { "label": "결제 금액(부가세 포함)", "value": "55,000원" }
  ]
}

# after the user agrees
curl -X POST https://api.hrum.ai/api/v1/partners/v1/console-ai/credit/topup-link \
  -H "Authorization: Bearer $HRUM_CONSOLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"planKey":"credit_50000","consentCode":"cc_…"}'

Endpoints

All are under /api/v1/partners/v1/console-ai.

Method · pathWhat it doesPermissionConsent
POST /consentsCreate a consent request ({action, payload})The action's permission (none for the first token)—
GET /consents/{code}Consent status: pending · approved · denied · expired · usedread (poll secret for the first token)—
GET /me · GET /tokenMy account · this token's permissionsread—
GET /statusGetting-started checklist (API · hosting · payment status and next steps)read—
GET /keysAPI key list (prefixes only)read—
GET /credit · GET /credit/transactionsCredit balance · history (offset, limit ≤ 100)read—
GET /usageCalls and spending by period (from, to)read—
GET /hostingHosting eligibility · plan · app statusread—
GET /seller · GET /payoutsSeller status (excluding personal identity data) · payout historyread—
POST /keysIssue an API key ({label?})keyskey_issue
POST /keys/{id}/rotateIssue a new key + turn this key off immediatelykeyskey_rotate
POST /keys/{id}/revokeRevoke a key (cannot be undone)keyskey_revoke
POST /credit/topup-linkTop-up payment link ({planKey})creditcredit_topup_link
POST /credit/refund-previewCalculate the refundable amount (does not request a refund)creditcredit_refund_preview
POST /hosting/applyApply for hosting ({planKey, subdomain, recurringConsent?})hostinghosting_apply
POST /hosting/apps/{id}/cancelCancel hostinghostinghosting_cancel
POST /seller/apply-linkLink to the seller registration screensellerseller_apply_link

The body of a write request is the same value as the consent request's payload + consentCode. A new API key is shown only once, in that response.

Card numbers and seller identity data are never accepted through the AI path. Card payments are made in the Toss payment window, and seller identity data, bank account and terms consent are entered by a person on console screens. If the payload contains values other than the defined fields, it is rejected with 400 UNKNOWN_FIELD.

Error codes

CodeHTTPMeaning
MGMT_TOKEN_INVALID401Token missing, expired or revoked. Get a new one
MGMT_SCOPE_DENIED403This token lacks the required write permission (scope field). Issue a new one with that permission checked
ACCOUNT_SUSPENDED403Suspended account
CONSENT_REQUIRED400No consentCode in the write request
CONSENT_PENDING409The person has not agreed yet
CONSENT_DENIED403The person declined
CONSENT_EXPIRED410Expired after 10 minutes
CONSENT_USED409Consent code already used
CONSENT_MISMATCH409The current request differs from what was agreed (amount · target)
CONSENT_NOT_FOUND404Unknown code, or a code from another account
INVALID_CONSENT_PAYLOAD400Invalid payload value (field field)
UNKNOWN_FIELD400Contains a field the AI path does not accept
CONSENT_RATE_LIMITED429Too many consent requests
MGMT_TOKEN_LIMIT40910 live tokens already, so no more can be issued
NOT_AVAILABLE404Menu not open yet

Use it through MCP

Install the HRUM MCP (@hrum/mcp 0.2.0) to use the flow above directly as tools. Tokens, API keys and poll secrets never appear in the model (the conversation); they are stored only in this computer's config folder and .env.local.

claude mcp add hrum -- npx -y https://console.hrum.ai/mcp/hrum-mcp-0.3.6.tgz
ToolWhat it does
hrum_ai_connect · hrum_ai_connect_statusCreate a management token consent link → save the token once approved (the value is not shown)
hrum_status · hrum_keys_list · hrum_credit · hrum_credit_history · hrum_usage · hrum_hosting_status · hrum_seller_status · hrum_payouts_listLookups
hrum_consent_request · hrum_consent_statusRequest write consent · check status
hrum_key_issue · hrum_key_rotate · hrum_key_revokeIssue and reissue keys (new keys are saved only to .env.local) · revoke
hrum_topup_link · hrum_refund_previewTop-up link · refundable amount
hrum_hosting_apply · hrum_hosting_cancel · hrum_seller_apply_linkApply for and cancel hosting · seller registration link

Account lookup API

APIs for checking your credit balance and usage. The header must include X-Partner-Api-Key.

GET /api/v1/partners/v1/credits/balance

Gets the current credit balance.

Request

curl -X GET https://api.hrum.ai/api/v1/partners/v1/credits/balance \
  -H "X-Partner-Api-Key: your-api-key"

Response

{
  "success": true,
  "data": {
    "balanceWon": 25000,
    "lastUpdated": "2026-10-09T05:12:30.000Z",
    "estimatedMonthEndBalance": 15000,
    "warningThreshold": 5000,
    "lowBalanceActive": false
  }
}

warningThreshold differs per partner — max(₩5,000, 20% of your most recent paid top-up). lowBalanceActive tells whether the balance has dropped below this threshold and the low-balance warning is active. There is no endpoint for topping up through the API — top up in console > Billing. This API has no error codes of its own — only the common errors.

GET /api/v1/partners/v1/usage/logs?limit=50&offset=0

Gets API call records, newest first. Each item has id · timestamp · endpoint (short name, e.g. manseryeok) · statusCode · costWon · responseTimeMs (currently always null), plus idempotencyKey · model · endUserId · cachedTokens when they apply.

Request parameters

Parameter Type Description
limit number Number of items to fetch (default: 50, max: 200)
offset number Offset (default: 0)
endpoint string Filter by endpoint short name (e.g. manseryeok). all or omitted for everything
dateFrom · dateTo string Period (YYYY-MM-DD). No limit if omitted

Request

curl "https://api.hrum.ai/api/v1/partners/v1/usage/logs?limit=50&offset=0&endpoint=manseryeok" \
  -H "X-Partner-Api-Key: your-api-key"

Response

{
  "success": true,
  "data": {
    "logs": [
      {
        "id": "12345",
        "timestamp": "2026-08-10T14:30:00.000Z",
        "endpoint": "manseryeok",
        "statusCode": 200,
        "costWon": 50,
        "responseTimeMs": null,
        "idempotencyKey": "order-8812"
      }
    ],
    "total": 1234,
    "pagination": {
      "limit": 50,
      "offset": 0,
      "hasMore": true
    }
  }
}

GET /api/v1/partners/v1/usage/summary?dateFrom=2026-08-01&dateTo=2026-08-31

Gets usage statistics for a period. If dates are omitted, the whole current month. Dates in dailyBreakdown are in Korean time (KST).

Request parameters

Parameter Type Description
dateFrom string Start date (YYYY-MM-DD)
dateTo string End date (YYYY-MM-DD)

Request

curl "https://api.hrum.ai/api/v1/partners/v1/usage/summary?dateFrom=2026-08-01&dateTo=2026-08-31" \
  -H "X-Partner-Api-Key: your-api-key"

Response

{
  "success": true,
  "data": {
    "dateRange": { "from": "2026-08-01", "to": "2026-08-31" },
    "totalCalls": 1234,
    "totalCostWon": 6170,
    "byEndpoint": [
      {
        "endpoint": "manseryeok",
        "calls": 1234,
        "costWon": 6170,
        "percentOfTotal": 100
      }
    ],
    "dailyBreakdown": [
      {
        "date": "2026-08-10",
        "calls": 150,
        "costWon": 750
      }
    ]
  }
}

Error codes (usage logs · summary)

Besides the common errors, a date not in YYYY-MM-DD format or a negative limit or offset returns a 400 with no code. Lookups are free.

GET /api/v1/partners/v1/status/upstream

Collects and returns the public status of the AI providers HRUM uses (OpenAI · Google Gemini · Anthropic Claude). No API key needed, free, and cached for 60 seconds. The status page uses this value.

curl https://api.hrum.ai/api/v1/partners/v1/status/upstream

{
  "success": true,
  "data": {
    "providers": [
      { "name": "OpenAI", "indicator": "none", "description": "All Systems Operational", "url": "https://status.openai.com", "checkedAt": "2026-10-12T03:00:00.000Z" },
      ...
    ],
    "checkedAt": "2026-10-12T03:00:00.000Z"
  }
}

indicator: none (operational) · minor · major · critical · unknown (status could not be read). This API has no error codes of its own.

Guest trial

A free API for trying Saju without logging in.

POST /api/v1/partners/v1/playground/manseryeok

Calculates the Manseryeok (the Korean Four Pillars calendar) without authentication. The guest endpoints (Manseryeok · natal chart) share a free quota of 5 calls per IP per month; beyond that, 429 GUEST_QUOTA_EXCEEDED is returned.

Request

curl -X POST https://api.hrum.ai/api/v1/partners/v1/playground/manseryeok \
  -H "Content-Type: application/json" \
  -d '{
    "year": 1990,
    "month": 1,
    "day": 15,
    "hour": 14,
    "minute": 30,
    "timezone": "Asia/Seoul",
    "gender": "male"
  }'

Response

Inside data you get the same natal chart and analysis fields as the paid Manseryeok API (wonkuk · ohaengBalance · yongshin · daeun · seun …) plus the remaining count guestRemaining. Lunar dates (calendar · isLeapMonth), location correction (coordinates · country), zaSiRule and birthTimePrecision are applied the same as in the paid API, and lunar input adds lunarInput. Because it is a trial, lang translation, glossary, identity and ohaengDeficient are not included. If timezone is not recognized, warnings are added as in the paid API.

{
  "success": true,
  "data": {
    "wonkuk": {
      "year": { "stem": "기", "branch": "사", "stemHanja": "己", "branchHanja": "巳", ... },
      "month": { ... }, "day": { ... }, "hour": { ... }
    },
    "trueSolarTime": null,
    "ohaengBalance": { ... },
    "yongshin": "...",
    ...,
    "guestRemaining": 4
  }
}

Limit exceeded (429)

{
  "success": false,
  "statusCode": 429,
  "code": "GUEST_QUOTA_EXCEEDED",
  "message": "게스트 체험 한도(월 5회)를 초과했어요. API 키를 발급받아 사용해주세요.",  // "Guest trial limit (5 per month) exceeded. Please get an API key."
  "timestamp": "..."
}

Error codes

HTTPCodeWhen
429GUEST_QUOTA_EXCEEDEDMore than 5 calls this month from the same IP (combined with the natal chart guest endpoint)
400INVALID_LUNAR_DATE · INVALID_LOCATIONThe lunar date does not exist / only one of latitude and longitude was sent. These also use up one call
400(none)Body validation failed (same as the common errors)

POST /api/v1/partners/v1/playground/natal-chart

Calculates a natal chart without authentication. The request body is the same as the paid /natal-chart, and the response is the same chart plus guestRemaining. Reading paragraphs (readings) are not provided. The quota is shared with the Manseryeok guest endpoint (5 per IP per month).

curl -X POST https://api.hrum.ai/api/v1/partners/v1/playground/natal-chart \
  -H "Content-Type: application/json" \
  -d '{ "birthDate": "1990-05-15", "birthTime": "12:00", "birthLatitude": 37.5665, "birthLongitude": 126.978, "birthTimezoneOffset": 540 }'

{
  "success": true,
  "data": {
    "sun": { ... },
    "moon": { ... },
    ...,
    "guestRemaining": 3
  }
}

Errors: 429 GUEST_QUOTA_EXCEEDED when the quota is exceeded; otherwise the same as the common errors.

Valid values

Heavenly Stems (Cheongan)

갑 (Gap), 을 (Eul), 병 (Byeong), 정 (Jeong), 무 (Mu), 기 (Gi), 경 (Gyeong), 신 (Sin), 임 (Im), 계 (Gye)

Earthly Branches (Jiji)

자 (Ja), 축 (Chuk), 인 (In), 묘 (Myo), 진 (Jin), 사 (Sa), 오 (O), 미 (Mi), 신 (Sin), 유 (Yu), 술 (Sul), 해 (Hae)

Five Elements (Ohaeng)

목 (Wood), 화 (Fire), 토 (Earth), 금 (Metal), 수 (Water)

Language-independent code values

The fields below stay as Korean code values even when you send lang. They are values for branching and lookups, not display names. For display names, use the translated fields in the same response (primaryLabel · signEn, etc.) or the Manseryeok glossary: true glossary. The values are the Heavenly Stems, Earthly Branches and Five Elements above, plus the Ten Gods (비견 Bigyeon · 겁재 Geopjae · 식신 Siksin · 상관 Sanggwan · 편재 Pyeonjae · 정재 Jeongjae · 편관 Pyeongwan · 정관 Jeonggwan · 편인 Pyeonin · 정인 Jeongin).

Candor (candor)

t, f (default) — case-insensitive. The old value blunt is normalized to t, and balanced · gentle to f. Any other value returns 400 (Candor (working name))

Error codes

Every API returns the common errors below in the same way. Each API section's error table lists only the codes specific to that API. Requests are checked in the following order; if a request fails at one step, later steps do not run.

  1. Security rules — blocked User-Agents are filtered before reaching the API server (403, HTML body, error 1010). This happens before the API key is checked, so it is 403, not 401
  2. Service protection cap — per-minute cap on all requests from the same IP (429, no code)
  3. API key — X-Partner-Api-Key check (401)
  4. Feature enabled — a feature not enabled for the account (404 *_not_enabled)
  5. Request limits — per-second and daily limits set in the console (429 RATE_LIMIT_EXCEEDED)
  6. Body validation — field names, types, ranges (400)
  7. Balance · calculation · call — insufficient balance (402), API-specific input errors, external model errors

Common errors

HTTP Code When Charged
400(none)Body validation failed — a field not in the table was sent, or a type or range is wrong. message is an array of stringsNo
401(none)X-Partner-Api-Key missing, invalid key, revoked key, inactive accountNo
402INSUFFICIENT_BALANCEInsufficient credit balance. In streaming responses, event: error carries the same code and statusNo
402(none)The balance was short at the moment of deduction (an API without a pre-call balance check, or another request used the balance during the call). message contains the required amount and remaining balanceNo
403(HTML body)Blocked by security rules (error 1010) — an HTTP client whose default User-Agent is blocked. See the User-Agent note in the QuickstartNo
404*_not_enabledA feature not enabled for the account (e.g. ziwei_not_enabled, dream_not_enabled)No
404(none)Unknown path. The body message is just "Not Found"No
429(none)Service protection cap exceeded (300 per minute per API key, etc.). No Retry-After — try again within a minuteNo
429RATE_LIMIT_EXCEEDEDPer-second or daily limit set in the console exceeded. The body has retryAfterSeconds and there is a Retry-After headerNo
500(none)Server error. Any amount already deducted for this request is returned after the responseRefunded
503SERVICE_DEGRADEDTemporary database connection problem. Retry-After: 30 — try again after 30 secondsRefunded

Error response body format: {"success": false, "statusCode": 400, "code": "INVALID_LOCATION", "message": "...", "timestamp": "..."} — code is included only when there is one. Branch on statusCode and code; do not rely on the message text.

// 401 — key missing or invalid
{ "success": false, "statusCode": 401, "message": "Invalid or missing API key", "error": "Unauthorized", "timestamp": "2026-10-12T03:00:00.000Z" }

// 400 — body validation failed (message is an array)
{ "success": false, "statusCode": 400, "message": ["property foo should not exist", "month must not be greater than 12"], "error": "Bad Request", "timestamp": "2026-10-12T03:00:00.000Z" }

// 404 — feature not enabled for the account (message: "This feature is not available yet")
{ "success": false, "statusCode": 404, "code": "ziwei_not_enabled", "message": "아직 열리지 않은 기능입니다", "timestamp": "2026-10-12T03:00:00.000Z" }

// 429 — request limit set in the console (message: "Per-second request limit exceeded. You can adjust it in console > API keys > Request limits.")
{ "success": false, "statusCode": 429, "code": "RATE_LIMIT_EXCEEDED", "message": "초당 요청 제한을 초과했어요. 콘솔 > API 키 > 요청 제한에서 조정할 수 있어요.", "retryAfterSeconds": 1, "timestamp": "2026-10-12T03:00:00.000Z" }

Frequently seen API-specific codes: 422 input that cannot be processed (PROMPT_TOO_LONG, EXTRACTION_FAILED), 503 temporarily unavailable (e.g. reading paragraphs could not be loaded; not charged), 504 external model timeout. The guest trial quota is 429 GUEST_QUOTA_EXCEEDED.

Response warnings (warnings)

When a request succeeds but part of the input was not recognized and a default was used, a warnings array is added at the end of the top level of data in the success response. The status code stays 200 and the price is the same. If there are no warnings, the warnings key is absent — the response is exactly as before. Each item is only {"code", "field"}; the values you sent (birth data) are not echoed back. If the same problem affects several fields, one item comes per field.

CodeMeaningFields
UNKNOWN_TIMEZONEThe time zone name was not recognized, so the default time zone was used. Check for typos (e.g. Asia/Seol). No warning if omitted or emptytimezone of Manseryeok · Saju Identity · Saju ↔ MBTI comparison · Today's Saju · Lifetime Saju · guest Manseryeok, birth.timezone of KB explain, personABirthTimezone · personBBirthTimezone of Compatibility
// 200 — calculated with the default time zone, only a warning is added
{ "success": true, "data": { "wonkuk": { ... }, ..., "warnings": [ { "code": "UNKNOWN_TIMEZONE", "field": "timezone" } ] } }

Rate Limit

A service protection cap always applies: 300 per minute per API key (3,000 per minute for all key requests from the same IP). Beyond that, 429 is returned without code or Retry-After — try again shortly (within a minute). Lower limits are set by the partner — set requests per second and requests per day (KST) in console API keys > Request limits to protect your balance from key leaks or infinite loops. If left empty, only the service protection cap above applies.

Billing

Every API call is deducted from your credits. New sign-ups get ₩1,000 in free credits (the grant may be withheld if fraud-prevention conditions are triggered). Guests can test for free up to 5 times a month.

API Price
manseryeok (Manseryeok) ₩50 per call
compat (Compatibility) ₩20 per call
lifetime (Lifetime Saju) Full ₩100 per call · one topic ₩20 per call · zodiac / Zi Wei Dou Shu layers +₩20 per layer
ziwei-chart (Zi Wei Dou Shu (Purple Star) chart) ₩50 per call · readings: true +₩20 (₩70 in total)
identity (Saju Identity, legacy endpoint) ₩10 per call — for new integrations, use the Manseryeok identity field (no extra charge)
saju-mbti (Saju ↔ MBTI comparison) ₩10 per call (same price as Saju Identity)
today (Today's Saju) ₩10 per call
dream (Dream Interpretation) ₩30 per call (₩0 if there is no result)
natal-chart (natal chart) ₩20 per call · readings: true ₩40 per call
personality-type (Personality Type) - free ₩0 (free)
personality-type (Personality Type) - premium ₩10 per call
reading (AI Saju Reading) Token-based (varies with the number of tokens used)
image (Saju image) ₩200 per image
image_generation (image generation · model resale) Fixed price per image by model, resolution and quality — price table

Guest testing

Idempotency

Every successful request is charged separately. Sending the same request again recalculates or regenerates it and charges again, so handle retry duplicates and reuse of the same input by storing responses on the client (e.g. a cache keyed by a request hash).

The X-Idempotency-Key header (up to 128 characters — anything longer is recorded as the first 128) is still accepted but does not prevent charging. The key you send is recorded as is in idempotencyKey in the usage logs, so you can use it to match which request was handled by which call against your own order or job IDs.

Exception: for Face & Palm reading, sending the same key and the same body again within 24 hours returns the first result without a new charge (Retries and resends).

Exception: for Name Analysis, sending the same key and the same body again within 24 hours returns the first result without a new charge (Retries and resends).

Support

Let us know if you get stuck during integration or a response differs from the docs.

InquiryContact
Technical support · outage reports · partnerships contact@float.do

Sending the following along with your inquiry helps us check faster.

For terms and personal data handling, see the Terms of Service (Korean) and the Privacy Policy (Korean).