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.
Let AI build your app — free ebooks (Korean)
Choose a book (Korean)All-in-one fortune app (Korean)Character chat app (Korean)Members-only web app (Korean)Apps in Toss mini app (Korean)
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.
- 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.
- Send a request - Send a POST request following the example below.
- 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.
- Changed (2026-10-14): Korean response sentences now use one consistent polite style; no content or field changes
- Added (2026-10-14): If a time zone name is not recognized, a successful response carries
warnings(UNKNOWN_TIMEZONE). Calculation still uses the default time zone as before, and with no warning the response is the same as before — Response warnings - Added (2026-10-17):
birthin AI Saju Reading, Reading Memory and Report now accepts the same birthplace, time zone and leap-month fields as the Manseryeok API (isLeapMonth,timezone, coordinates,country,zaSiRule,birthTimePrecision). If you don't send them, calculation is the same as before — AI Saju Reading API
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_FLAGGED | 400 | Blocked by moderation (ridm: system+user, resale: user messages) |
INSUFFICIENT_BALANCE | 402 | Insufficient balance (checked by reserving the full requested output up front) |
MODEL_NOT_FOUND | 400 | Model not configured |
INVALID_TOOLS / TOOLS_UNSUPPORTED_FOR_MODEL | 400 | Mismatched tools fields / tool calling requested on a Claude model |
FILE_NOT_FOUND | 400 | fileIds includes a file that doesn't exist (or belongs to another partner) |
FEATURE_DISABLED | 403 | The youtube, files or emotion option was used on an account with that feature turned off |
RATE_LIMITED | HTTP 400, body status 429 | Upstream rate limit, or the daily resale spending cap was exceeded (default ₩100,000/day per partner) |
UPSTREAM_TIMEOUT | HTTP 400, body status 504 | Upstream response timed out (60 seconds) |
UPSTREAM_INVALID_REQUEST | 400 | The upstream model rejected the request (upstream 400/422) |
INVALID_RESPONSE_FORMAT | 400 | response_format: json_object but the messages don't contain the word "json" (OpenAI family) |
REPORT_ONLY_MODEL | 400 | ridm/saju-report was specified — it's only for the Saju Report API |
| (none) | 400 | user missing for a ridm/* model · systemPrompt over 8,000 characters · unsupported model prefix |
provider_not_configured | 503 | Claude models are not connected right now |
Notes
- YouTube transcript injection: With youtube:true, the transcript is extracted from the YouTube link in the last message and included in the input tokens. If no transcript can be fetched, the conversation continues without injection and
youtubeUsedcomes back as an empty array (only the standalone YouTube Summary API returns 400 with no charge). - Original model compatibility: If the model value is openai/* or google/*, anthropic/*, the original model is called as is, without memory features (cost × exchange rate × 1.10).
- Prompt cache discount: On OpenAI-family models (openai/*, ridm/gpt-*), if you repeatedly send the same prefix (1,024 tokens or more), input tokens that hit the upstream cache are billed at 30% of the input price. You can check this in the response's
usage.prompt_tokens_details.cached_tokensand incachedTokensfromGET /usage/logs; whether the cache hits can vary per call depending on upstream policy. Google models have no cache, so the full input price applies. Claude models don't request caching either, so the full input price applies. - REPORT_ONLY_MODEL excluded: ridm/saju-report is one-shot only with no memory, so it can't be used on this endpoint (400).
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
- Field cap: Up to 100 fields per end user. Beyond that, the oldest fields are removed first.
- Value length: 150 characters per field by default, up to 500. Adjustable with profileValueMaxChars.
- Cumulative storage: Values extracted with extractProfileFields accumulate; empty or null values are skipped and the existing value is kept. Fields are deleted only through the DELETE API above (or removed oldest-first automatically past 100). Values set with endUserProfile overwrite extracted values.
- Value length is per request: profileValueMaxChars is not stored and applies only to values stored in that request.
- Soft Update: Existing values are kept even if the schema (field names) changes, and are merged with new values on every call.
- ridm/* only: Resale models (openai/*, google/*) ignore the profile.
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)
- OpenAI (23):
openai/gpt-6-astra·openai/gpt-6-sol·openai/gpt-6-luna·openai/gpt-5.6-sol·openai/gpt-5.6-terra·openai/gpt-5.6-luna·openai/gpt-5.5·openai/gpt-5.4·openai/gpt-5.4-mini·openai/gpt-5.4-nano·openai/gpt-5.2·openai/gpt-5.1·openai/gpt-5·openai/gpt-5-mini·openai/gpt-5-nano·openai/gpt-4.1·openai/gpt-4.1-mini·openai/gpt-4.1-nano·openai/gpt-4o·openai/gpt-4o-mini·openai/o3·openai/o3-mini·openai/o4-mini - Google (3):
google/gemini-3.5-flash·google/gemini-3.5-flash-lite·google/gemini-3.1-pro - Anthropic (13): See the Claude models section below
- RIDM: For each model above there is a memory model with the same name and a
ridm/prefix (resale price × 1.3), plusridm/saju-reportfor Saju reports only (called only through/reading/report).
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 theridm/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 400TOOLS_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 |
|---|---|
interpretation | Interpretation text |
sajuSource | computed (calculated from birth) | provided (used the saju input) |
sajuMismatch | Warning when birth and saju were both sent and the pillars differ ({pillars, computed, note}); otherwise null/omitted |
toolsUsed | Tools that actually ran (e.g. ["youtube"]) |
toolsSkipped | Tools requested but skipped [{tool, reason}], reason: unavailable | cap_exceeded | error | auto_rejected |
filesUsed | Injected document IDs (when tools.fileIds is used) |
usage | Token counts for reference (inputTokens is the actual value including Saju and system prompts — not the billing basis) |
costPoints | Amount actually charged (₩) |
modulesUsed | Modules actually included (e.g. ["saju","ziwei","astro"], saju first). Present only when the included-modules feature is on |
moduleFees | Fixed fee per included module (₩) (e.g. {"ziwei":50,"astro":20}). Already part of costPoints. Present only when the included-modules feature is on |
tarot | The drawn cards when Tarot is included — same shape as the Tarot API response (with imageUrl in cards[], without question and costPoints) |
only | Echoes the request's only value (only if sent). If only isn't saju, Saju isn't used, so there is no sajuSource |
support | Crisis 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 |
|---|---|
| 400 | MISSING_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) |
| 402 | INSUFFICIENT_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 |
| 403 | FEATURE_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.
| Module | When it's included | What goes in as evidence | Price (= one call of the standalone endpoint) |
|---|---|---|---|
Zi Wei Dou Shu ziwei | By 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 astro | By 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 dream | When you send a dream in dream | The dream text + symbol words found in the dream | /dream (KRW 0 if there are no candidates) |
Tarot tarot | When you send tarot | The cards drawn in this request and their upright/reversed meanings (returned in tarot with image URLs) | /tarot |
- Billing: On top of the usual token price (question + persona + evidence blocks of included modules, characters × 0.25 as input tokens, plus output tokens), each included module adds the price of one call to its standalone endpoint. Each evidence block is a summary under 1,500 characters. Modules whose feature is turned off for the account or that have no price are not included. Per-module prices and the expected range per call are on the pricing page.
- Requests that send only the four pillars (
saju) have no birth date, so Zi Wei Dou Shu and astrology are not included (Dream Interpretation and Tarot are). - Relation to
tools.astrology: When the astrology module is included, the chart injection fromtools.astrologyis skipped so the same natal chart isn't added twice. When the astrology module is left out (excludeor feature off),tools.astrologyworks as before. - Dream Interpretation safeguard: If the
dreamtext contains crisis language, the AI is not called and help information (support) is returned withcostPoints: 0. - Feature switch: Currently on. If the server setting is turned off,
exclude,dreamandtarotare ignored even if sent, and the reading, price and response are the same as before included modules existed (nomodulesUsed,moduleFeesortarot).
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.
only | What it calls | Required input | Price |
|---|---|---|---|
saju | Saju AI (Saju only, no other modules) | birth or saju | Token price only (no fixed fee) |
ziwei | Zi Wei Dou Shu AI | birth + birth time (hour) | Token price + one call of /ziwei-chart |
astro | Astrology AI | birth (time not required) | Token price + ₩20 (fixed fee for the astrology module) |
tarot | Tarot AI | tarot options — no birth needed | Token price + one call of /tarot |
dream | Dream Interpretation AI | The dream text in dream — no birth needed | Token price + one call of /dream |
- Billing: Token price (question + persona + only that AI's evidence block, characters × 0.25 as input tokens, plus output tokens) + that module's one fixed fee. The expected price per call for each domain is on the pricing page.
- Can't be combined with
exclude: Sending both returns 400ONLY_WITH_EXCLUDE. If the input that AI needs is missing, you get 400ONLY_REQUIRES_INPUT; if that feature is turned off for the account, 400ONLY_DOMAIN_UNAVAILABLE— it doesn't fall back to a Saju reading. If the chart can't be calculated for the date, 400ONLY_INPUT_INVALID. tarotanddreamdon't need a birth date. Just send the question and the cards (or the dream text), withoutbirthorsaju. Anonlyother thansajudoesn't use Saju, so the response has nosajuSource.- Feature switch: Follows the same server setting as included modules, and is currently on. If the setting is turned off,
onlyis ignored and the call behaves like a regular Saju reading.
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
- Only the interpretation is returned: The response contains only the generated interpretation text; internal prompts and Saju theory are not exposed.
candorApplied: Only whencandorwas sent astdoes the response carry the candor actually applied (t|f). Same for/reading/memory(Candor (working name)).- Billing basis: Input = (question + persona characters) × 0.25 tokens (+ YouTube transcript characters injected in
/reading× 0.25); output = actual completion tokens. Saju injection and system prompts are not billed, andusage.inputTokensis for reference. When included modules (today's fortune · Zi Wei Dou Shu · Astrology · Dream Interpretation · Tarot) are on, their evidence blocks and per-module fixed fees are added. costPoints is the amount actually charged. - Response language: Questions not in Korean are answered in the same language as the question.
- Unknown birth time: If hour is omitted, the hour pillar is left out and the reading uses the three pillars of year, month and day.
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.):
| HTTP | Code | When |
|---|---|---|
| 400 | MISSING_ENDUSER_ID | endUserId is empty or only whitespace |
| 400 | MISSING_SUBJECT_INPUT · INVALID_SAJU_PILLAR · INVALID_LUNAR_DATE | Birth data or Saju input error (same as the Reading API) |
| 400 | UNKNOWN_TOOL · INVALID_TOOL_MODE · INVALID_FILE_IDS · FILE_IDS_TOO_MANY · INVALID_FILE_ID_TYPE · FILE_NOT_FOUND | tools or fileIds error. This path also accepts tools.emotion and tools.summaryEvery |
| 400 | ONLY_WITH_EXCLUDE · ONLY_REQUIRES_INPUT · ONLY_DOMAIN_UNAVAILABLE · ONLY_INPUT_INVALID | Errors related to Just one AI |
| 402 | INSUFFICIENT_BALANCE | Couldn't reserve the output cost up front (same method as the Reading API) |
| 403 | FEATURE_DISABLED | A 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.):
| HTTP | Code | When |
|---|---|---|
| 404 | END_USER_NOT_FOUND | Unknown endUserId (common to the four paths other than saving a conversation) |
| 400 | MISSING_PARAM | Search — endUserId missing |
| 400 | MISSING_TAGS | Search — tags is empty |
| 400 | ENDUSER_ID_TOO_LONG | Saving a conversation — endUserId is longer than 255 characters |
Notes
- 1.3× rate: Because of memory hosting, input/output token rates are 1.3× those of AI Saju Reading (per 1M tokens: input KRW 3,754 · output KRW 30,030, excl. VAT).
- endUserId required: Each end user needs a unique ID; calls with the same ID keep past context.
- Memory retention: Stored memory is kept until you explicitly delete it.
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 |
|---|---|---|---|
| birth | object | Either 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") |
| saju | object | Either birth or saju | Enter the four pillars directly: {"year":{"stem":"경","branch":"오"},"month":{…},"day":{…},"hour"?:{…}} |
| year / month / day / gender | number / string | Optional | Flat fields (legacy, solar). Accepted as birth only when all four are present. With neither birth nor saju, you get 400 MISSING_SUBJECT_INPUT |
| hour / minute | number | Optional | If 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 |
| tone | string | Optional | warm (default) | casual | formal |
| candor | string | Optional | Candor — 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) |
| persona | string | Optional | Speech style / character instructions (up to 2,000 characters — only this length is billed as input) |
| sections | string[] | Optional | List 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)
}
}
- Saju injection is free: Manseryeok calculation and injection and the system prompt are not billed. Input billing covers only the persona length; output is the tokens actually used.
- Measured cost: One report with the default 8 sections (about 3,000 characters) costs about 85P (based on output at ₩38,077/1M).
- Retries are billed again. If you want to reuse a response for the same input, store it on the client side (Idempotency).
Error codes
Codes this API returns in addition to the common errors (401, 400 body validation, 402, 429, 503, etc.):
| HTTP | Code | When |
|---|---|---|
| 400 | MISSING_SUBJECT_INPUT | Neither birth nor saju was sent |
| 400 | INVALID_SAJU_PILLAR | A pillar in saju is not one of the 60 sexagenary pairs |
| 400 | INVALID_LUNAR_DATE | The lunar date doesn't exist |
| 402 | INSUFFICIENT_BALANCE | Couldn'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
- Captions required: no captions → TRANSCRIPT_UNAVAILABLE 400 (not billed).
- Errors: invalid URL or ID 400
INVALID_YOUTUBE_URL/ summary generation failed 400LLM_GENERATION_FAILED/ insufficient balance 402INSUFFICIENT_BALANCE. - Billing: billed by tokens, at the resale price as is (cost × 1.1) of the model used (default
ridm/gpt-5.6-terra) — there is no memory layer, so the RIDM Memory rate (× 1.3) does not apply. costPoints = ceil(input tokens × input rate + output tokens × output rate); rates are per 1M tokens (excl. VAT). - Measured cost: about 60P per call for a 20–30 minute video (tokens included).
- Use with readings: in the AI Saju Reading API (
/reading),tools: { "youtube": true }injects the captions of links in the question (a top-levelyoutubefield is ignored). YouTube injection works only on /reading (not yet on /reading/memory). If you send it to /reading/memory, captions are not fetched andtoolsSkippedreports{"tool": "youtube", "reason": "unavailable"}.
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_YOUTUBE_URL | No video ID found in the URL |
| 400 | TRANSCRIPT_UNAVAILABLE | The video has no captions (not billed) |
| 400 | LLM_GENERATION_FAILED | Summary generation failed or returned nothing |
| 402 | INSUFFICIENT_BALANCE | Could not reserve the summary cost in advance |
| 403 | FEATURE_DISABLED | The 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.
- Disabled account-wide: responds 403 { code: 'FEATURE_DISABLED', feature: 'youtube' }
- Per request:
youtube:truemissing or false = no YouTube injection
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
- Upload and extraction: free (no tokens billed for text extraction)
- AI analysis (analyze=true): the actual tokens used for boundary detection + per-section summaries + the overall summary are billed at the analysis model's rate. Synchronous for ≤30,000 characters, asynchronous for >30,000 (status:
analyzing) - Chat injection: document text counts as input tokens and is billed like any chat request
- Logs: shown in the console usage log as
file_upload(free) andfile_analyze(paid, actual tokens)
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.
- Disabled account-wide: responds 403 { code: 'FEATURE_DISABLED', feature: 'files' }
- Per request: no
fileIds= no document injection (handled normally)
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
- Format check: unsupported formats return 400 { code: 'INVALID_FILE_TYPE' }
- Other errors: missing file 400
FILE_NOT_PROVIDED/ over 10MB 400FILE_TOO_LARGE/ unknown file 404FILE_NOT_FOUND/ 400 if:idis not a UUID - Required for analysis: analyze=true without endUserId returns 400 { code: 'END_USER_REQUIRED' }
- Extraction failure: an error while extracting the document returns 422 { code: 'EXTRACTION_FAILED' }
- Balance check: partner credits are checked before an analysis request. If short, 402 is returned
- Automatic knowledge-base loading: with analyze=true, each section's summary and tags are saved to the knowledge base automatically and become auto-recall candidates. If endUserId is given, they are also added to that user's memory layer.
- Re-upload = new file: every upload creates a new file (new id), and with analyze=true it is analyzed and billed again. Upload a document once and reuse the returned id in
fileIds.
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 400 | FILE_NOT_PROVIDED | Upload — no file part |
| 400 | INVALID_FILE_TYPE | Upload — unsupported format |
| 400 | FILE_TOO_LARGE | Upload — over 10MB (sometimes a 413 with no code) |
| 400 | END_USER_REQUIRED | Upload — analyze=true but no endUserId |
| 400 | (none) | Details / delete — :id is not a UUID |
| 402 | INSUFFICIENT_BALANCE | Upload — could not reserve the analysis cost in advance |
| 403 | FEATURE_DISABLED | Document processing is turned off for the account (all four routes) |
| 404 | FILE_NOT_FOUND | Details / delete — file does not exist or belongs to another account |
| 422 | EXTRACTION_FAILED | Upload — 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:
- Global:
partner_emotion_enabled✓ - Partner: "Emotion analysis" is turned on in the console's feature settings
- Request:
emotion: trueis set - 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).
- summaryEvery=1: emotion updates on nearly every turn (the summary runs asynchronously after the response)
- summaryEvery=5: every 5 turns (recommended)
- summaryEvery=20: every 20 turns (fewest updates)
- Summary and emotion cycles are never billed to the partner, whatever the interval.
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)
- improving: non-positive emotions (sadness+anger+fear+disgust) dropped by 50 or more
- stable: change under 50
- declining: non-positive emotions rose by 50 or more
- null: no data from a previous cycle
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
- No separate charge: emotion analysis runs inside the summary-cycle prompt, and summary and emotion cycle tokens are not billed to the partner. You pay only for the input and output tokens of the chat turn itself.
Notes
- Before the first cycle: there is no emotion field until the first summary cycle has run (expected behavior).
- Feature off: an
emotion: truerequest while the global or partner feature setting is off returns 403 { code: 'FEATURE_DISABLED', feature: 'emotion' } (same as YouTube and document processing). If you don't sendemotionor send false, the field is simply omitted. - Supported on ridm/* models only. On accounts with the emotion feature on, the emotion field is ignored for resale (openai/*, google/*) requests; on accounts with it off,
emotion:truereturns 403FEATURE_DISABLEDregardless of model. - Streaming: in streaming responses, emotion is included in the message_done SSE event.
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
- Fixed price: every image generation is billed ₩200.
- Generation time: generating an image takes 20–30 seconds. Set a generous timeout.
- Base64 encoding: the returned imageDataUri is a base64-encoded PNG and can be used directly in an HTML
<img src="{imageDataUri}">tag. - Styles: background (background art), icon (minimal icon) and portrait (portrait illustration) change only the mood. All styles have the same size (default 1024×1024 square).
- Timeout: if the upstream does not return an image within 80 seconds, 504
IMAGE_UPSTREAM_TIMEOUTis returned and nothing is billed. Try again. - Unknown birth time: if hour is omitted, the hour pillar is dropped and the reading uses the three pillars of year, month and day.
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 402 | INSUFFICIENT_BALANCE | Balance is less than the price of one image |
| 504 | IMAGE_UPSTREAM_TIMEOUT | No 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
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | O | openai/gpt-image-2, openai/gpt-image-1-mini, google/nano-banana-pro, google/nano-banana, fal/nano-banana-pro, fal/nano-banana |
prompt | string | O | Description 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 |
size | string | GPT-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. | |
quality | string | GPT-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_ratio | string | Nano 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. | |
resolution | string | Nano Banana only: Nano Banana supports 1K only; Nano Banana Pro supports 1K (default) · 2K · 4K. Uppercase K only. | |
n | number | Number 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": "..."
}
- GPT-Image and Google direct models return images as
b64_json; FAL models return aurl. costWon= per-image price × images actually generated. If the upstream returns no image at all, nothing is billed.- The
usage(token counts) in GPT-Image responses is for reference only and is not used for pricing.
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 400 | MODEL_NOT_FOUND | Model is not in the price list |
| 400 | INVALID_MODEL | Prefix other than openai/, google/ or fal/ |
| 400 | INVALID_PARAMETER | An option the model does not accept (aspect_ratio or resolution on GPT-Image, quality on Nano Banana, an unsupported resolution) |
| 400 | EMPTY_PROMPT | The prompt is only whitespace |
| 400 | INVALID_PROMPT | The upstream model rejected the prompt |
| 400 | UPSTREAM_ERROR | Upstream error or zero images (not billed) |
HTTP 400, body status 429 | RATE_LIMITED | Upstream rate limit — try again shortly |
| 402 | INSUFFICIENT_BALANCE | Per-image price × n exceeds the balance |
| 422 | PROMPT_TOO_LONG | Prompt 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.
| status | Body |
|---|---|
pending | Still generating — poll again after the Retry-After header / retryAfter |
succeeded | result = exactly the 200 response a synchronous call would have returned (data, costWon, requestId) |
failed | error = exactly the error body a synchronous call would have returned (statusCode, code, message) — not billed |
- Billing: deducted once, at the moment the result is ready. While waiting, the expected amount is reserved from the balance; on failure or timeout the reservation is released and nothing is billed. Polling many times does not bill again.
- Retention: results and errors can be fetched for 24 hours after the job is created and are deleted after that (404 afterwards). If images are
b64_json, the poll response is just as large (several MB per image) — save it as soon as you receive it. If the whole result exceeds 16MB, it is not stored and the job fails withIMAGE_JOB_RESULT_TOO_LARGE(not billed) — reduce the number of images (n) or the resolution. - Another partner's job ID, an unknown ID and an expired ID all return the same 404
IMAGE_JOB_NOT_FOUND.
| HTTP | Code | When |
|---|---|---|
| 202 | — | A Prefer: respond-async request did not finish within 60 seconds — poll with the job ID above |
| 404 | IMAGE_JOB_NOT_FOUND | Unknown, expired or another partner's job ID |
| 429 | IMAGE_JOB_POLL_RATE_LIMITED | Polled again within 2 seconds with the same API key — wait Retry-After seconds |
error.statusCode 504 | IMAGE_UPSTREAM_TIMEOUT | No image received within 240 seconds (not billed) |
error.statusCode 413 | IMAGE_JOB_RESULT_TOO_LARGE | Result exceeds 16MB (not billed) |
error.statusCode 503 | IMAGE_JOB_INTERRUPTED | Job 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
- Large response: the Manseryeok response contains not only the natal chart but detailed data such as 10-year luck, annual luck, Gyeokguk and Sinsal. Pick only the fields you need.
- Optional fields — the unknown-birth-time contract:
hourandminutecan be omitted. Ifhouris omitted, noon is not assumed; the hour pillar is excluded. The response changes as follows.wonkuk.hour=null,trueSolarTime=nullohaengBalance= recalculated from the three pillars of year, month and day (six stem + branch characters)branchRelationsandcheonEulGwiIn= only entries formed by the three pillars' branches remain- A top-level
birthHouris added:{ known: false, hourPillar: null, basis: "three_pillars", excludedFields: [], note }. Analysis fields (yongshin,strength,gyeokguk,hapChungReport,ilganSummary,seun, etc.) are not removed; they are returned as values calculated from the three pillars (six characters) (thresholds that depend on the character count are also converted to a six-character basis). These are values derived from the three pillars alone, not noon reference values - To detect an unknown birth time, use
birthHour.known— notexcludedFields.length. That array is now always empty and kept only for compatibility with older clients minuteis ignored if sent withouthour(minute ?? 0only whenhouris present)- Requests that send
hourhave nobirthHourfield, and results are the same as before (exceptidentityandohaengDeficient, added since 2026-10-10). Existing clients that always sendhourare not affected. Clients built for the briefly existing "calculate at noon when omitted" behavior should checkbirthHour.known === falseand show the hour-pillar cell as "unknown".strength,gyeokguk, etc. are always returned as three-pillar values (before 2026-09-16 they were removed). The consultation APIs (/reading,/reading/memory,/reading/report) follow the same rule and pass three-pillar values to the model
- Korean field values: all field values (Heavenly Stems, Earthly Branches, Five Elements, etc.) are written in Korean. Sending
langdoes not change field names; only values (Yongsin, Day Master description, 10-year luck direction, Sinsal names, etc.) are translated. Stem, branch and element codes (stem,branch,ohaeng) and the keys ofohaengBalancestay in Korean in every language. For the full list, see Valid values > Language-independent code values. - All extra options are optional (2026-10-08): if you send none of
lang,calendar,isLeapMonth,birthLatitude,birthLongitude,country,zaSiRuleorbirthTimePrecision, results are the same as before — except that since 2026-10-10identityandohaengDeficientare always appended to the end of the response (values and order of existing keys unchanged). The price is still ₩50/call. - Change (2026-10-12): the lunar conversion basis was switched to Korea Astronomy and Space Science Institute (KASI) data, changing results for some dates. A non-existent leap month is no longer converted to the regular month; it returns 400
INVALID_LUNAR_DATE. - Lunar input: with
calendar: "lunar", the conversion detailslunarInputare added at the top level of the response. The true solar time correction goes intrueSolarTime. Actual engine output (lunar 1990-07-15 14:30, Busan coordinates,lang: "en"):{ "year": 1990, "month": 7, "day": 15, "hour": 14, "minute": 30, "timezone": "Asia/Seoul", "gender": "male", "calendar": "lunar", "birthLatitude": 35.1796, "birthLongitude": 129.0756, "lang": "en" } → "lunarInput": { "calendar": "lunar", "lunarDate": "1990-07-15", "isLeapMonth": false, "leapMonthApplied": false, "solarDate": "1990-09-03" }, "trueSolarTime": { "originalLocal": "14:30", "correctedLocal": "14:07", "longitudeCorrection": -23.6976, "equationOfTime": 0.3241, "totalCorrectionMinutes": -23.4, "dayShift": 0, "dstApplied": false, "basis": "coords", ... }, "ilganSummary": "辛 Metal — Jewel energy. Sharp intellect and refined aesthetic sense.", "daeunDirection": "Forward" - Input errors: a lunar date that does not exist (e.g. the 30th of a 29-day month) returns 400
INVALID_LUNAR_DATE, and a request with only one of latitude/longitude returns 400INVALID_LOCATION; neither is billed. Format errors such as a missingtimezoneare also 400 (not billed).
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_LUNAR_DATE · INVALID_LOCATION | Non-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.
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
year · month · day | number | O | Birth year (1900-2100), month and day (solar by default; lunar with calendar: "lunar") |
hour · minute | number | Birth time. If hour is omitted, calculated with the same unknown-birth-time rule as the Manseryeok API (three pillars of year, month and day) | |
timezone | string | O | Time zone (e.g. Asia/Seoul). An unrecognized name is calculated in the default zone and the response gets a warning (UNKNOWN_TIMEZONE) |
gender | string | O | "male" or "female" |
topic | string | To get a single topic: personality, wealth, career, love, health, relationships, advice. All seven topics if omitted | |
lang | string | ko (default) · en · ja · th | |
layers | string[] | 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 · 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). Lunar requests get lunarInput in the response (same format as the Manseryeok API) | |
birthLatitude · birthLongitude | number | Birthplace 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 | |
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). layers also use this country (inferred from lang if omitted) | |
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). Used only for the Saju paragraphs (Zi Wei Dou Shu always rolls over to the next day at 23:00) | |
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 |
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" }
}
}
topicsalways 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).keyis the Korean determination value (independent of language). There are 0–3ohaeng_stateparagraphs 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), sendcountry: "KR". For people born abroad,timezonealone 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
layersused 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;emptyif there is no main star).ziwei_palace: the per-topic palace paragraph.keyispalace 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 nolabel(the first palace paragraph gets alabelonly 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.ziweireturns"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.notegives the reason. Ifcountryis not sent, the layers infer the birth country fromlang(ko → Korea, ja → Japan, th → Thailand). reasonis one ofbirth_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
topicset, ₩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 islifetime. - 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:
| HTTP | Code | When |
|---|---|---|
| 404 | lifetime_not_enabled | The feature is not enabled for the account |
| 400 | INVALID_LUNAR_DATE · INVALID_LOCATION | Non-existent lunar date / only one of latitude and longitude sent |
| 402 | INSUFFICIENT_BALANCE | Insufficient balance when the amount including the requested layers was checked in advance |
| 503 | lifetime_narrative_unavailable | Reading 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 acountryCode(KR·JP·TH), the time is corrected to true solar time. The result is inclock. - Transformations: Sanhe school star placement + birth-year Four Transformations. The engine is pinned to iztro 2.6.1 (
chart.engine).
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
year · month · day | number | O | Birth year (1900-2100), month, day |
hour | number | O | Birth hour (0-23). A Zi Wei Dou Shu chart cannot be built without the birth hour — if omitted, you get 400 birth_time_required |
minute | number | Birth minute (0-59), 0 if omitted | |
gender | string | O | "male" or "female" (sets the direction of the decade periods) |
calendar | string | solar (default) or lunar | |
isLeapMonth | boolean | true 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 | |
birthTimePrecision | string | exact (true solar time correction) or approximate (no correction + notes.approximateTimeNote). No correction if omitted | |
birthLatitude · birthLongitude | number | Birthplace coordinates (for exact correction) | |
countryCode | string | Country 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 | |
birthTimezoneOffset | number | UTC offset of the birth time (minutes, 540 = KST) | |
referenceDate | string | Reference date for decade/annual periods, YYYY-MM-DD (default: today, Korea time) | |
lang | string | ko (default) · en · ja · th | |
timezone | string | For compatibility with the Manseryeok API (not used in the calculation, so no warning is added even if the name is not recognized) | |
readings | boolean | If 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 "化禄".
palacesalways 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 bykey(ming,siblings,spouse,children,wealth,health,travel,friends,career,property,fortune,parents).mutagenslists the birth-year Four Transformations (Lu, Quan, Ke, Ji) and the palace each transformed star sits in. Themutagenon a star object is also the birth-year transformation.featuresis 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-starco·meet(co:A+B+C=co:A+Bandco:A+C; same formeet) and relation keys involving minor stars (count them fromstar:<minor star>@<branch>andpal:·shen:·limit:: opposite palace +6, three-directions-and-four-squares 0·±4·+6, 夾 ±1).- Original-text quotes in rule source cards: in the
sourcesof 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 locationfoundAt— is returned without the original text (quote) and location (location) and with"citation": "pending"(source being verified). The rule statement andsourceIdstay as they are, and quotes confirmed in the original (match: "EXACT"),PARTIALquotes 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. currentDecadalisnullbefore the starting age of the first decade period.yearly.yearis 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 }.mingis the paragraph for the Life-palace major-star combination (key·text·lang),topicsholds a paragraph per topic (topic·text·lang), andpalacesholds a paragraph per palace and major star (palace·star·text·lang). A paragraph with no text in the requested language comes in Korean, and itslangisko.missingSectionsis 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.versionmarks 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 isziwei_chart. - Other errors that are not charged: 404
ziwei_not_enabled(feature not enabled for the account) · 404ziwei_readings_not_enabled(reading paragraphs not enabled) · 503ziwei_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:
| HTTP | Code | When |
|---|---|---|
| 404 | ziwei_not_enabled | The feature is not enabled for the account |
| 404 | ziwei_readings_not_enabled | readings: true but reading paragraphs are not enabled |
| 400 | invalid_birth_date | A solar date that does not exist, or a lunar day / leap month out of range |
| 400 | birth_time_required | No hour — partial comes with it, as in the example above |
| 400 | reference_date_before_birth | referenceDate is before the birth date |
| 402 | INSUFFICIENT_BALANCE | Insufficient balance |
| 503 | ziwei_readings_unavailable | Reading 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:
/identityresponses carryDeprecation: trueandLink: </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 ofidentity. 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/identityfor 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 oflang. For display names, sendglossary: truein the Manseryeok request and use the translations in the returned glossary (glossary, in thelanglanguage). For the full list, see Valid values > Language-independent code values. - Unknown birth time:
/identitycalculates with noon, while Manseryeok'sidentityuses the same three pillars (year, month, day) as the Manseryeok body. When/identitycalculates on three pillars (birthHour.basis=three_pillars),saju.houris{ "cheongan": "", "jiji": "", "ohaeng": null }(since 2026-10-16ohaengisnull, meaning there is no hour pillar). The hour pillar is not included in the Five Elements counts. - Pricing: the existing
/identityis ₩10/call (only when enabled for the account; otherwise 404identity_not_enabled). Manseryeok'sidentityhas no extra charge. The usage-log endpoint isidentity.
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:
| HTTP | Code | When |
|---|---|---|
| 404 | identity_not_enabled | Not enabled for the account |
| 400 | INVALID_LUNAR_DATE · INVALID_LOCATION | A lunar date that does not exist / only one of latitude and longitude sent |
| 402 | INSUFFICIENT_BALANCE | Insufficient 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 acceptcurrentYear·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 isbalanced. Matched rule IDs are inruleIds·matchedRules. - Comparison (
comparison): same direction givesrelation: "same"("same direction as the Saju"), a different one gives"different"("developed a side different from the Saju"), and if either side isbalancedit is"neutral". The sentences (text) are currently Korean only.langaffects onlybirthHour.note. - Unknown birth time: if you leave out
hour, it calculates on the same basis as Saju Identity and addsbirthHour. On the three-pillar basis,wonkuk.hourisnullandpillarCountis 3. - Caveat rule:
caveatRuleId(R-MBTI-CAVEAT-001) andcaveatscontain 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:
| HTTP | Code | When |
|---|---|---|
| 400 | (body validation) | mbti missing, not 4 letters, axis value not an integer 0~100, unknown key |
| 400 | INVALID_LUNAR_DATE · INVALID_LOCATION | A lunar date that does not exist / only one of latitude and longitude sent |
| 402 | INSUFFICIENT_BALANCE | Insufficient balance |
| 404 | saju_mbti_not_enabled | The 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
date | string | Date to look at, YYYY-MM-DD (1900-2100). If omitted, today in Korea time | |
temperatureC | number | Current 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) | |
weather | string | Weather: clear · cloudy · rain · snow · storm | |
humidity | number | Relative 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 | |
mbtiBehavior | boolean | If 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.typeis 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).labelis the human-readable name.flow_score·flow_tierdescribe the flow of that year (annual luck, Seun), andtoday_mbtiis 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 }, andverdictisfavored·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 whentemperatureCis sent.fashion.mood: only whentemperatureC·weather·humidityare all sent.mbtiBehavior: only withmbtiBehavior: 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 thetaskAffinity·daily_flow·samjae_label·fashion·mbtiBehaviorstrings are all translated. Omitted orkomeans 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, andbirthHouris added. Lunar input addslunarInput.
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:
| HTTP | Code | When |
|---|---|---|
| 404 | today_not_enabled | The feature is not enabled for the account |
| 400 | INVALID_LUNAR_DATE · INVALID_LOCATION | A lunar date that does not exist / only one of latitude and longitude sent |
| 402 | INSUFFICIENT_BALANCE | Insufficient 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
| Parameter | Type | Required | Description |
|---|---|---|---|
dream | string | ✓ | The dream (up to 1,000 characters) |
feeling | string | How 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) | |
lang | string | Response language. Only ko is supported for now (the dictionary is Korean-only). Any other value returns 400 | |
includeNegative | boolean | 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 }
]
}
}
itemsholds 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), andscoreis 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).feelingis 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·meaningfields were removed and replaced bykeywords. On the same day, the source of symbol words changed to HRUM's own symbol glossary, so each entry now has one word andmodehas two values,glossary·none(the old valuestags·fallback_charno 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": ["구렁이", "길"]
}
]
statementreports what the original text recorded. It is not a prophecy that the dream foretells anything.quoteis 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 withincludeNegative: true.caveatRuleIds: caveat rules attached to that rule (e.g. records of death or illness carryR-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:
| HTTP | Code | When |
|---|---|---|
| 404 | dream_not_enabled | The feature is not enabled for the account |
| 402 | INSUFFICIENT_BALANCE | Insufficient 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
| Parameter | Type | Required | Description |
|---|---|---|---|
modelName, modelVersion | string | ✓ | The model that produced the coordinates. Outside the allowed list: 400 MEASURE_MODEL_UNSUPPORTED / MEASURE_MODEL_VERSION_MISMATCH. Current: face_landmarker · float16/[email protected] |
normalization | string | ✓ | face-norm-1 |
landmarks | number[][] | ✓ | Exactly 478 [x, y] or [x, y, z] (divided by image width and height, −0.02~1.02) |
imageWidth, imageHeight | number | ✓ | 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) | |
quality | object | yawDeg·pitchDeg·rollDeg·blendshapes (MediaPipe coefficient name → score). Quality checks run only if you send it | |
includeNegative | boolean | Whether 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
| Parameter | Type | Required | Description |
|---|---|---|---|
modelName, modelVersion | string | ✓ | Current: hand_landmarker · float16/[email protected] |
normalization | string | ✓ | palm-norm-1 |
landmarks | number[][] | ✓ | Exactly 21 |
imageWidth, imageHeight | number | ✓ | Size in pixels |
handedness | string | ✓ | left | right — the subject's actual hand (let the user choose) |
mirrored | boolean | true for a horizontally flipped photo (selfie preview) | |
creases | object[] | 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) | |
autoLabel | boolean | Names 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 | |
includeNegative | boolean | Same 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.pydoes. Leave real, visible breaks broken (the number of breaks is counted by thebreakskey).
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 isnull, with the reason inreasons: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 isnull(keys and reasons are inkeys·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).statementis a record of what the original text says, not a prophecy or diagnosis.negativeonly withincludeNegative: 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 arenull. - 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):statementrecords 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
| HTTP | code | Meaning |
|---|---|---|
| 400 | MEASURE_MODEL_UNSUPPORTED | modelName is not on the allowed list |
| 400 | MEASURE_MODEL_VERSION_MISMATCH | modelVersion is not on the allowed list (currently only float16/[email protected]) |
| 400 | MEASURE_NORMALIZATION_MISMATCH | normalization is not face-norm-1 / palm-norm-1 |
| 400 | MEASURE_LANDMARK_COUNT | Point count is not 478 for a face / 21 for a hand |
| 400 | MEASURE_LANDMARK_SHAPE · MEASURE_COORD_NAN · MEASURE_COORD_RANGE · MEASURE_IMAGE_SIZE · MEASURE_DEGENERATE | Errors in point format, numbers, range (−0.02~1.02) or image size |
| 400 | MEASURE_HANDEDNESS · MEASURE_CREASE_LIMIT · MEASURE_CREASE_SHAPE · MEASURE_CREASE_LABEL | Errors in hand or palm-line input |
| 400 | MEASURE_QUALITY_POSE · MEASURE_QUALITY_FACE_SIZE | Head 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 |
| 402 | INSUFFICIENT_BALANCE | Insufficient credit balance |
| 404 | face_measure_not_enabled · palm_measure_not_enabled | Feature 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_handinquality.flags), every key isnull(reasonback_of_hand), the KB rules are empty, and nothing is charged (₩0). Send again with a palm-side photo and the correcthandedness·mirrored. The back-of-hand check compares the winding direction of the wrist, index-finger base and little-finger base points againsthandedness·mirrored. - The price is the same even if some keys are
null. The number of missing keys is given inmissingSections.
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
| Parameter | Type | Required | Description |
|---|---|---|---|
image | object | One 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) |
landmarks | object | One of the two | The Face Measurement request body as is (modelName · modelVersion · normalization · landmarks 478 points · imageWidth · imageHeight · hairline · quality · includeNegative) |
consent | object | ✓ for photos | Consent to photo processing — see the consent object below |
llm | boolean | No need to send it — the AI reading is always included. Sending false returns 400 USE_FACE_MEASURE (measurement without AI is Face Measurement) | |
lang | string | Accepts ko · en · ja · th, but reading text is currently Korean only (response lang = "ko") | |
endUserId | string | The 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
| Parameter | Type | Required | Description |
|---|---|---|---|
image | object | ✓ | One palm-side photo. Same format and size limits as face reading |
consent | object | ✓ | Consent object |
creases | object[] | 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 | |
lang | string | Same as face reading (reading text currently Korean only) | |
endUserId | string | Same 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]] }
]
}
Consent object consent
Example: "consent": { "photo": true, "version": "photo-consent-2026-10", "subject": "end_user" }
photo: must betrue— 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 onlyversionandsubject.
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 400PALM_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'smeta.processingstates 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. Facekey:face_shape·forehead·eyebrows·eyes·nose·mouth·ears·chin. Palmkey:hand_shape·fingers·life_line·head_line·heart_line·fate_line·sun_line·mounts.labelcomes in Korean.- A face reading sent as
landmarksalso returns the Face Measurement response fields (normalization·keys·reasons·quality·kbMatches·missingSections) under the same names, anditemshas one entry per KB rule. As in a photo reading,labelis the feature name (eyebrows · eyes · nose …) andreasonis the AI's plain-language reading, together withkey(measurement key) ·ruleId·polarity·source(the KB rule statement the reading is based on).meta.kbRuleIdscollects the source rule IDs. meta.disclaimeris alwaysentertainment_reference. Showmeta.disclaimerText(or wording with the same meaning) on the user's screen.
Error codes
| HTTP | code | Meaning |
|---|---|---|
| 400 | READING_INPUT_ONE_OF | Face: not exactly one of image and landmarks |
| 400 | IMAGE_REQUIRED | Palm: no image |
| 400 | CONSENT_REQUIRED | A photo was sent but consent is missing or invalid |
| 400 | IMAGE_INVALID · IMAGE_FORMAT_UNSUPPORTED | Unreadable photo · mimeType differs from the actual format · not JPEG, PNG or WebP (including animated WebP) |
| 413 | IMAGE_TOO_LARGE | Photo over 8MB · over 8192px or 16.8 megapixels · WebP long side over 1600px |
| 400 | IMAGE_NOT_ALLOWED | Appears to be a minor · several faces / two or more hands · not a face or palm photo |
| 400 | FACE_NOT_DETECTED · PALM_NOT_DETECTED | No face / palm found (a photo of the back of the hand is PALM_NOT_DETECTED) |
| 400 | USE_FACE_MEASURE | llm: false with face landmarks — measurement without AI is the Face Measurement API |
| 400 | MEASURE_LOW_QUALITY | Blurry · dark · obscured · too small · tilted — the reason is in the response's quality. Ask the user to retake the photo |
| 400 | CREASE_INVALID | A palm line has fewer than 2 points or points outside 0~1 |
| 400 | MEASURE_* | landmarks input errors (same codes as Face Measurement) |
| 402 | INSUFFICIENT_BALANCE | Insufficient balance — checked before calling the external model |
| 404 | face_reading_not_enabled · palm_reading_not_enabled | Feature not open yet |
| 409 | IDEMPOTENCY_KEY_REUSED · IDEMPOTENCY_IN_PROGRESS | The 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) |
| 429 | FILTERED_QUOTA_EXCEEDED | Today's filtered photos reached the daily limit — see Daily limit on filtered photos below |
| 502 | READING_UNAVAILABLE | The reading could not be produced (including truncated output) — please try again |
| 503 | IMAGE_BUSY | Photo processing is busy — please try again shortly |
| 504 | LLM_UPSTREAM_TIMEOUT | The 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
landmarkscall 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
consentis required. - Name calculation (
"interpret": false): calculation only; AI is not called.interpretationisnullandconsentis 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(responselang="ko").
POST /api/v1/partners/v1/naming
| Parameter | Type | Required | Description |
|---|---|---|---|
name.hangul | string | ✓ | 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 | |
birth | object | { 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 | |
interpret | boolean | Default true (name reading). false means name calculation only (no AI) | |
echoName | boolean | Added 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) | |
consent | object | ✓ 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 |
lang | string | Accepts ko · en · ja · th, but currently answers in Korean only | |
endUserId | string | The 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 withstrokeNote: "disputed". For any other Hanja, send the original stroke count inname.strokesto get the four patterns. If even one syllable has no stroke count, you getsuri: 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). itemscarriesdeterministicin 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
| HTTP | code | Meaning |
|---|---|---|
| 400 | NAME_INVALID | Hangul name is not 2~6 syllables · format error in hanja/strokes/meanings values |
| 400 | NAME_HANJA_LENGTH | hanja length differs from the number of syllables |
| 400 | CONSENT_REQUIRED | Name reading without consent, or with invalid values |
| 400 | (validation error) | A date that does not exist · a field not in the documentation |
| 402 | INSUFFICIENT_BALANCE | Insufficient balance |
| 404 | naming_not_enabled | Feature not open yet (name reading and name calculation separately) |
| 409 | IDEMPOTENCY_KEY_REUSED · IDEMPOTENCY_IN_PROGRESS | The 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) |
| 502 | READING_UNAVAILABLE | The AI interpretation could not be produced — not charged (no partial charge for returning only the calculation). Please try again |
| 504 | LLM_UPSTREAM_TIMEOUT | The 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.disclaimerisentertainment_reference. Showmeta.disclaimerTexton 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.
- Build an app with Tarot alone — draw the card names, meanings and images from the response straight onto the screen.
- Tarot + LLM interpretation — pass the drawn cards into the LLM API and write your own reading.
- Combine with Saju AI — add
tarotto an AI Saju Reading request and the Tarot is read together with the Saju reading (included modules).
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
count | number | △ | Number of cards to draw, 1~10 (integer). Send only one of count and spread. Sending both or neither returns 400 |
spread | string | △ | 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) |
reversed | boolean | Allow reversed cards (default true). With false, all cards are upright | |
seed | string | A 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) | |
question | string | The 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 | |
lang | string | Language 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
}
}
cards[]:position(from 1, draw order = spread position) ·positionLabel(spread position name,nullwhen drawn withcount) ·id(language-independent card ID) ·name(requested language) ·nameEn·arcana(major|wands|cups|swords|pentacles) ·reversed·keywords·meaning(for the drawn orientation) ·imageUrl.spread:nullwhen drawn withcount.- Card images:
imageUrl·cardBackUrlare WebP (600px wide), and the URL contains a version (v2), so it does not change — you can fetch, cache and reuse them. Show reversed cards by rotating the image 180 degrees (there is only one, upright, image per card). - Image source: scans of the original 1909 Rider-Waite deck (Pamela Colman Smith, public domain). All 78 cards and the back are 1909 original-deck scans from Wikimedia Commons; per-file sources and licenses are in the source ledger. The old URLs (
v1) return the same images, so there is no need to change URLs you already stored. costPoints: the amount deducted for this call (₩).
Pricing
- One flat fee per call — the same whether you draw 1 card or 10. The price is in the calculation APIs table under pricing. Only successful calls are charged, and 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
tarot. - Calling it before it is open returns 404
tarot_not_enabled(not charged).
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.
kb.cards[]:position·id·reversed·drawn(meaning for the drawn orientation) ·upright·reversedMeaning·positionRule. Each meaning hasruleId·status·statement(Korean) ·quotes[](quoteoriginal English text,locationsection,foundAtline in the original) ·variants[](other descriptions Waite gave,alternative; additional meanings from Part III §4,additional).statususes the KB marking as is (verified= original quote present). Where Waite gave no meaning (Two of Cups reversed), it isruleId: null,status: "disputed".method: "celtic-cross": the Celtic method of divination Waite describes in the book (10 cards). Send it instead ofcount·spread; Waite's meaning for each of the 10 positions comes inpositionRule. Otherspreads are layouts not in Waite's text, so they have no position source (kb.method: null).kb.caveats: Waite's note that meanings change with position, and usage principles (it is a tool for divination and does not replace health, legal or financial decisions).- Sending
methodbefore it is open returns 404tarot_kb_not_enabled(not charged).
{
"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:
| HTTP | Code | When |
|---|---|---|
| 404 | tarot_not_enabled | The feature is not enabled for the account |
| 404 | tarot_kb_not_enabled | method was sent but the source-text feature is not open yet |
| 402 | INSUFFICIENT_BALANCE | Insufficient balance |
Send exactly one of count·spread·method. Two or more, or none, gets the common 400.
Natal Chart API (Western astrology) Discontinued
/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, orbirthTimePrecision: "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
langisko. - If the reading paragraphs cannot be loaded: 503
natal_readings_unavailable(not charged).
Notes
- Price: ₩20 / call (pay as you go) · +₩20 with
readings: true(₩40 total) - Date validation: an invalid date (e.g. Feb 30) or invalid coordinates/timezone returns a 400 error and is not charged.
- Ascendant calculation: needs the birth time +
birthTimePrecision(exact/approximate;exactif omitted) + latitude/longitude. If any is missing,ascendantisnull. - Reading paragraph errors:
readings: truewithout the feature enabled for the account gives 404natal_readings_not_enabled; failure to load the paragraphs gives 503natal_readings_unavailable— neither is charged. - Sun/Moon/Ascendant: every planet and angle uses the tropical zodiac (the Western astrology standard).
- Changed 2026-10-17 (
engineVersionnatal-2): if you sendbirthTimeand omitbirthTimePrecision, that time is now used (previously the time was ignored, the chart was calculated at noon and the Ascendant wasnull). This is the same rule as other endpoints. Calls withoutbirthTimeor with an explicit precision give the same results as before.
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 404 | natal_readings_not_enabled | readings: true but reading paragraphs are not enabled |
| 503 | natal_readings_unavailable | Reading 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).
| Parameter | Type | Required | Description |
|---|---|---|---|
houseSystems | string[] | 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) | |
minorAspects | boolean | Include semi-sextile (30°), semi-square (45°), sesquiquadrate (135°) and quincunx (150°) (natal and synastry). Default false — the 5 major aspects only | |
orbs | string | Natal 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) | |
node | string | Which 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 | |
lang | string | Language of signName (sign names) — ko (default) · en · ja · th | |
timing | object | If sent, transits + secondary progressions are calculated and placed in the response's timing. See the table below | |
partner | object | The 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:
| Parameter | Type | Required | Description |
|---|---|---|---|
orbDegrees | number | O | Orb (degrees) for transit→natal and progressed→natal aspects, 0.5~10. The same value is used for every body pair (5 major aspects only) |
at | string | The 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 | |
progressions | boolean | Calculate secondary progressions (default true) | |
yearLength | string | The 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.
bodies[]: Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto.longitudeis ecliptic longitude (0° = vernal equinox, tropical),speedis degrees/day — negative meansretrograde: true. Each body carriessignName(sign name in thelanglanguage) ·degreeInSign(degree within the sign, 0~30°) ·house(house number per requested house system, the same value ashouses[system].bodies—{}if houses are not calculated), so you can draw a chart table from this alone.dignities: dignities of the Sun through Saturn (domdomicile ·exaexaltation ·detdetriment ·falfall, every state that applies). Same values asbodies[].dignities.nodes[]:MeanNode(mean node) ·TrueNode(true node) = North Node,south= South Node (+180°).angles: ASC·MC·DSC·IC. Calculated only when both the birth time (exact/approximate) and coordinates are present. Otherwisenull, andanglesOmittedgives the reason (time_unknown|coordinates_missing). In that case houses are not calculated either, andhousesis{}.houses: per requested system,cusps(starting ecliptic longitude of houses 1~12) +bodies(house number per body).houseSystems·houses: the systems actually calculated. For birthplaces inside the polar circles (angles.polar= latitude above about 66.56°), Placidus may be undefined, so Porphyry is calculated instead and marked withhouses.porphyry.fallbackFrom = "placidus"(it is never switched silently). The check uses the birthplace latitude, so the same birthplace always gets the same system regardless of time.housesExcludedlists any other systems that could not be calculated, with the reason (this is not an error response).aspects[]:orb= degrees off exact,strength=exact(within 2°) ·close(within half the limit) ·wide,applying=trueif moving toward exact (nullif unknown),outOfSign= an aspect that crosses a sign boundary (read one step weaker).moonSignUncertain: when the time is unknown,trueif the Moon changes sign during that day — in that case the Moon'splanet_signinfeaturescarries"uncertain": true.readings(always included, no extra charge): reading paragraphs pre-written by HRUM.dignities[]= dignities (Sun through Saturn, one strongest state per body),aspects[]= aspects between natal bodies, tightest orb first. Each paragraph haskey(same key as infeatures·aspects[].key) ·text·lang(the paragraph's actual language —koif there is no paragraph in the requested language) ·deviation(aspects only, degrees off exact). Only keys with a pre-written paragraph are included and the rest are skipped (nothing is written on the fly) — aspects involving Uranus, Neptune or Pluto and minor aspects have no paragraphs and are left out. Without a birth time, aspects involving the Moon are not included; if the Moon sign is uncertain, the Moon's dignity is not included. The example above (1990-05-15 14:30, Seoul) has 7 of 16 aspects + 4 dignities = 11 paragraphs.readingsUnavailable:trueif the reading paragraphs could not be loaded — the chart calculation still comes back as is,readingsis an empty array, and the price (₩50) stays the same.features[]: keys for matching against a rule database.planet_signSu_ari·planet_retrogradeMe_rx·dignityMa_dom·node_signMNn_leo·TNn_leo·Nn_leo·Sn_aqu(Nn·Snare the node chosen by thenodeoption) ·angle_signAsc_sco·planet_houseVe_h7(+system) ·aspectSu_Mo_cj(for nodes, conjunction only, 3° —Mo_Nn_cj·Sa_Sn_cj·Nn_Asc_cj; Ascendant pairs only when the birth time is present) ·house_cusp_signh12_ari(12th-house cusp in Aries) ·house_emptyh6(a house with none of the 10 planets) ·house_ruler_househ1_h7(ruler of the 1st house in the 7th) ·house_ruler_signh7_lib(ruler of the 7th house in Libra) — the house ruler is the traditional ruler of the cusp sign (the 7 planets from Sun to Saturn) ·planet_risingJu(in the 1st house, or in the 12th within 6° of the Ascendant) — these five kinds carry+systemand appear only when birth time and coordinates are present ·planet_solarVe_cazimi(within 17′ of the Sun) /Me_combust(17′~8°30′, in the same sign as the Sun) /Ma_beams(otherwise within 17°) — the three levels do not overlap ·moon_phasewaxing·waning·metatime_known(time present —exact·approximate) /time_hour_level(added forapproximate) /time_unknown. Aspect keys (aspect·transit·progressed·syn_aspect) carryorb(degrees off exact, two decimal places), plus"outOfSign": trueonly when the aspect crosses a sign boundary — values to match the rule-key attributes[orb=…]·[outOfSign=true]. If the birth time is unknown, natal aspects involving the Moon carry"uncertain": true. Body abbreviationsSu Mo Me Ve Ma Ju Sa Ur Ne Pl, nodesMNn TNn Nn Sn, aspect codescj(0°)sx(60°)sq(90°)tr(120°)op(180°) · minorssx(30°)ssq(45°)sqq(135°)qcx(150°).
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
}
}
- Transit targets (
natalPoints): the 10 natal bodies, plus the Ascendant (Asc) and Midheaven (MC) if birth time and coordinates are present. Keys use the prefixest(transit) andn(natal) —tSa_nSu_cj= transiting Saturn conjunct natal Sun. transit_housetSa_h7(transiting Saturn in the natal 7th house) ·progressed_housepMo_h4(progressed Moon in the natal 4th house): 10 planets each. Natal houses use the default system (Placidus; Porphyry +"fallbackFrom": "placidus"in polar regions), and the keys carry"system"— even if you send a differenthouseSystems, these two keys use the default system. They do not appear without birth time or coordinates.- Progressed→natal aspects target natal planets only. Key
pSu_nJu_tr= progressed Sun trine natal Jupiter.instantis the moment of the sky used for the progressed chart (birth +ageYearsdays). - With
progressions: false,timing.progressionsisnull. IfyearLengthis omitted, it istropical(365.24219 days). - With
progressionson, atiming.atearlier than the birth time returns 400at_before_birth(not charged). - If the time is unknown, the natal chart is set at noon. Keys targeting the natal Moon (
…_nMo_…) and progressed-Moon keys (pMo_…) carry"uncertain": true(the progressed Moon inherits the natal Moon position, so it varies by as much as the noon assumption).
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
}
}
- Key prefixes:
a= the requesting person,b=partner.aVe_bMa_cj= your Venus conjunct the partner's Mars. Every combination of your 10 bodies (+Ascendant, Midheaven) × the partner's 10 bodies (+Ascendant, Midheaven) is checked (direction matters, soaSu_bMoandaMo_bSuare different keys). - The Ascendant (
Asc) and Midheaven (MC) are calculated only when that person's birth time and coordinates are both present (same rule as for you; for the partner, seesynastry.partner.angles/anglesOmitted). Cross aspects involving angles are checked as conjunction and opposition only, at 6°. The one exception is Ascendant–Ascendant conjunction (aAsc_bAsc_cj), allowed up to 17° (Ptolemy's Tetrabiblos — two Ascendants within 17° make a strong bond, rule[orb=17]). The actual separation comes inorb. An angle is calculated whenever the time of the person who owns that angle is known. The Descendant and IC are exact opposites of the Ascendant and Midheaven, so they get no separate keys. astro_syn: the 2 strongest sign relations among the 6 classical pairs (Sun–Moon, Venus–Mars, Moon–Venus, Sun–Venus, Moon–Mars, Venus–Saturn). Both directions fold into one key (Su_Mo_se= one person's Sun and the other's Moon share an element).syn_sign: sign relations for every point of yours × every point of the partner's (aAsc_bAsc_ss). If both people's birth times areexact, the Part of Fortune (PoF, day/night formula reversed) is also included as a point (aPoF_bPoF_ss) — the Part of Fortune moves with the Ascendant at about 1° per 4 minutes, so it is not produced for hour-level (approximate) or unknown times. Relation codes:ss(same sign) ·op(opposite sign) ·se(same element) ·es(supporting element) ·sm(same modality) ·ef(element friction) ·nt(unrelated).- The orb table and minor-aspect setting use the same
orbs·minorAspectsas the natal chart. Between two natal charts there is no applying or separating, soapplyingis alwaysnull. - If either person's birth time is unknown, that person's chart is set at noon. Aspects involving that person's Moon carry
"uncertain": trueinfeatures(the Moon moves about 13° a day, more than the orb). The same applies to aspects paired with the partner's angles (aAsc_bMo_…, and in the other directionaMo_bAsc_…). - The partner's houses are not calculated (synastry house overlays are not provided).
Defaults and values you must choose
- House system (default
placidus): the system the 1819 and 1929 dictionaries describe as "in common use". In polar regions it is replaced by Porphyry and marked withfallbackFrom. If you need another system, choose it withhouseSystems. - Progression year length (default
tropical, 365.24219 days): the source texts' "birthday to next birthday" year. The difference from the Julian year (julian) is about 2.5 minutes of progressed time at age 80. - Transit and progression orb (
timing.orbDegrees, required when you sendtiming): the sources disagree (a range of 8° to 10° before exact). Leaving it empty returns 400. - Natal aspects involving Pluto, the nodes or the angles are not in the default orb tables: these are points the Leo (1928) and Heindel (1919) orb tables do not cover (Pluto was discovered in 1930). So these points do not appear in natal
aspects(they do appear in positions and houses). Only angle cross aspects in synastry are checked, as conjunction/opposition at 6° (Ascendant–Ascendant conjunction only at 17°).
Not provided
- Minor dignities (triplicity, terms, faces), reception, the natal Part of Fortune (used only in synastry
syn_sign), dignities of Uranus, Neptune and Pluto, parallels and midpoints - Progressed angles (progressed ASC·MC) — the sources do not define a method
- DSC·IC values are given, but no
featureskeys are produced for them (the South Node comes as theSnkey) - New Moon, Full Moon, first quarter and last quarter phase keys (
moon_phaseiswaxing·waningonly), oriental/occidental of the Sun, progressed-angle keys - Synastry house overlays
Pricing
- ₩50 per call, the same as Manseryeok — adding
timing·partnerblocks, house systems or minor aspects does not change it: one ₩50 charge. The response'scostPointsis the amount deducted (₩). The usage-log endpoint isastrology. - Deducted only when the calculation succeeds. Input errors (400) and insufficient balance (402) are not charged.
- Calling it before it is open returns 404
astrology_not_enabled(not charged).
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 404 | astrology_not_enabled | The feature is not enabled for the account |
| 400 | at_before_birth | timing.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
| Parameter | Type | Required | Description |
|---|---|---|---|
birthDate | string | O | YYYY-MM-DD (1900~2100). A date in the calendar given |
birthTime | string | null | HH:mm. If absent, the hour pillar is unknown — calculated on six characters, with hourKnown: false | |
calendar | string | O | solar · lunar (lunar dates are converted to solar for the calculation) |
isLeapMonth | boolean | Lunar leap month (default false) | |
gender | string | null | male · female. If absent, there is no 10-year luck layer (daeun: [], daeunIndex: null) | |
timezone | string | Only Asia/Seoul is accepted | |
birthLatitude · birthLongitude | number | Birthplace 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 | |
country | string | Country of birth, 2-letter code. Sending KR·JP·TH without coordinates corrects with that country's representative longitude (same as Manseryeok) | |
birthTimePrecision | string | exact · 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) | |
range | object | O | { "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 |
monthsOfYear | integer | null | The 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: []) | |
lang | string | ko · 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
line: cumulative value — for 10-year luck, natal chart + 10-year luck; for annual luck, natal chart + that year's 10-year luck + annual luck. Each step is clipped to ±100, withclamped: trueif clipped (R-SAJU-UN-033). Monthly luck has no cumulative line; only that month's share (delta) is given.share: the breakdown of one character's share (support, control, hidden stems, roots). Layer weights are 1:1:1 for 10-year, annual and monthly luck (R-SAJU-UN-030), and roots from luck branches have no cap (R-SAJU-UN-031).shinRole: the five-role name for that character's element (Yongsin, Huisin, Gisin, Gusin, Hansin — favorable, helpful, unfavorable, harmful and neutral elements; R-SAJU-YONG-170) — a name, not a score. Every chart gets a five-role table (nevernull).relations: the list of combinations, clashes and punishments between luck characters and natal characters (not added to the score).ruleIdis the pair-table rule,decisionRuleIdis the rule that justifies applying it to luck too (UN-035 monthly luck, UN-036 self-punishment, UN-037 half combination).withDaeunis annual luck ↔ that year's 10-year luck. If the hour pillar is unknown, relations with the hour pillar are not produced.triples: cases where a 10-year or annual luck character completes a three-character Samhap (triple combination) or Banghap (directional combination) together with the natal chart (R-SAJU-UN-037).hapPulim: natal combinations touched by 10-year or annual luck characters (combination triggered, released, broken) and newly formed combinations (R-SAJU-HAP-021~027). Monthly luck is not included. Build on-screen text yourself in the app's language fromreasonCodeandparams(incomingthe incoming luck character,origindaeun|seun, andtarget·partnerwhen needed).reasonFallbackis always present as a Korean sentence written by the engine, meant only as a fallback to show when the app receives areasonCodeit does not know. There are currently 7reasonCodes.stem_hap_kept_jaenghap: the natal Heavenly Stem combination stays intact, and a luck character of the same yang stem (incoming) cannot interfere because it is a competing combination (jaenghap).stem_hap_kept_tuhap: the natal Heavenly Stem combination stays intact, and a luck character of the same yin stem (incoming) cannot interfere because it is a jealous combination (tuhap).branch_hap_gi: a luck character (incoming) clashes with one character (target) of a natal six-combination, triggering the combination (hapgi).branch_hap_released: a luck character (incoming) punishes one character of a natal six-combination, releasing the combination.branch_hap_broken: a character identical to one character of a natal six-combination (incoming) arrives through luck, breaking the combination.new_stem_hap: a luck Heavenly Stem (incoming) and a free natal Heavenly Stem (partner) form a new stem combination.new_branch_yukhap: a luck Earthly Branch (incoming) and a natal Earthly Branch (partner) form a new six-combination.
startAge: the Daeun starting number (age of the first 10-year cycle). Days are counted from 00:00 of the birth date (Korean date) to the solar-term start, without the hour and minute of birth, and rounded with 3 days = 1 year (R-SAJU-UN-038). This rule has been in the ledger sincekbVersion2026.10.11-2.ipchunNote:trueon the current year's slot only, if the moment of the query (Korea time) is before that year's Ipchun. It marks that from Jan 1 to before Ipchun, the previous year's annual luck is still running. Annual luck uses that year's Ipchun stem-branch (R-SAJU-UN-016).birthBeforeIpchun:trueif born between Jan 1 and before Ipchun, so the natal year pillar is the previous year's stem-branch (top-level field).assumedLocation(sincesaju-flow-5, top-level field): the default used for the calculation when no location was sent. Currently{ "correction": "none" }(no true solar time correction), the same as Manseryeok. If Manseryeok's default changes to a representative longitude, you will get{ "correction": "trueSolarTime", "longitude": … }.nullif a location (birthLatitude·birthLongitudeorcountry) was sent.natalAmbiguous: when the time is unknown and a solar term starts on the birth date,{ "pillar": "month" | "year", "termAt" }— the engine decides at noon, so this marks that the month pillar (and the year pillar too, for Ipchun) may differ from reality.compatInputs: values that let the app do only set intersections when overlaying several people (Yongsin element, counts of visible characters' elements, elements not visible). No summing or ranking between people is produced.
Pricing
- If sales are switched off, it returns 404
saju_flow_not_enabled(not charged). - Deducted only when the calculation succeeds. Input errors (400 — a date that does not exist, a range over 101 years, etc.) and 409 are not charged. The response's
costPointsis the amount deducted (₩), and the usage-log endpoint issaju-flow.
Error codes
Besides the common errors (401, 400 body validation, 402, 429, 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 404 | saju_flow_not_enabled | The feature is not enabled for the account |
| 400 | INVALID_DATE | birthDate format error or a date that does not exist |
| 400 | INVALID_LUNAR_DATE | A lunar date that does not exist |
| 400 | INVALID_RANGE | toYear is before fromYear, or the range exceeds 101 years |
| 400 | INVALID_TIME | birthTime is not in HH:mm format |
| 400 | INVALID_LOCATION | Only one of birthLatitude·birthLongitude sent |
| 409 | saju_flow_correction_unsupported | A 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 / personABirthLongitudepersonBBirthLatitude / 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 · personAIsLeapMonthpersonBCalendar · 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).
- Time correction: each person's Saju (day pillar · eight-character score · overall score · intimacy symbol table · zodiac animal · authority analysis ·
yearFlow· relationship narrative) is calculated once and shared by every item. If you don't send coordinates,personXCountryor precisionexact, the input time is used as is (same as the Manseryeok API). If you need the same compatibility as the INTA app (Korean, time entered directly), sendpersonXCountry: "KR"(+personXBirthTimePrecision: "exact") for both people — see the recommended call below. People without a birth time are not corrected. The default (no correction unless sent) does not change. - Changed 2026-10-11: when coordinates are sent, the Saju also gets true solar time correction (before, they were used only for zodiac compatibility). This change makes Saju and zodiac use the same time base within one response, so calls that were already sending coordinates may see different Saju scores and day pillars. Responses for new fields and for calls without coordinates are unchanged.
- Day pillars are code values: the values of
saju.personA.iljuandsaju.personB.ilju(e.g.계미) are Korean stem-branch code values regardless oflang. For display names, convert them with the Manseryeok API glossary (glossary) or the Chinese-character form (癸未; for English and Thai癸未 (Gye-mi)).
Recommended call (same result as the INTA app)
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"
}
}
saju(Saju compatibility) is the main part. In rare cases where the day pillar cannot be calculated it isnull— then show onlyttisaju.score.total: 0–100. Average of the day-pillar table score and the eight-character analysis score (each inbreakdown).gradeis a plain-language grade — 아주 잘 맞아요 (very good match, 85+) · 잘 맞아요 (good match, 75+) · 무난해요 (fine, 65+) · 맞춰 가면 좋아요 (works with effort, 55+) · 서로 많이 달라요 (very different). This value is the same as before — the score that also includes the symbol table and zodiac isheaderBreakdown.averagesaju.axes: six axes (0–100) — attraction · stability · growth · communication · passion · longevity; they change withpurpose.saju.pattern: relationship type name and description (nullif none)saju.iljuAxes(from 2026-10-27): the same values as the six-axis chart on the INTA app result screen — the six axes of the compatibility table for the two people's day-pillar pair as is (attraction·stability·growth·communication·passion·longevity, 0–100, independent ofpurpose). The average of the six axes isheaderBreakdown.sixAxis. It has the same keys assaju.axes, but the calculation is different, so the numbers differ.nullif the pair is not in the compatibility tablesaju.elements: calculated from the Five Elements distribution of each person's whole natal chart (six characters if the time is unknown —basis: "six").bProvidesToA= elements A lacks that B hassaju.tenGods: B's Ten God as seen from A (aView) and the reverse (bView).highlights: cross Sinsal sentences found in the eight characters (up to 5, empty array if none)relations(from 2026-10-16): list of relations between the two natal charts. It looks at Earthly Branch pairs in the same position (year · month · day · hour — the hour pillar only when both people have a birth time), stem combinations and stem clashes between the two Day Masters, the Nayin element of the year pillars, and three-harmony (samhap) and directional (banghap) combinations that only form when the two charts are combined. Each item haspositions([A side, B side]—year·month·day·hour; for samhap and banghapchart= the whole chart),kind(table below),tier,ruleIds(IDs of the supporting KB rules — check the original source in the KB API) andchars(the related characters, in Chinese characters). All keys are language-independent, so they stay the same if you changelang. Build display text fromkindyourself. There are no good/bad verdict words.nullif the natal chart cannot be built, empty array if there are no relationstier:gathercoming together (the combination side) ·clashfriction ·heavyClashheavy friction (a relation the classic texts rank as heavier; currently clashes) ·listOnlylisted only (a relation for which no direction could be found in the classic texts, so it is not scored)p2Score(from 2026-10-16): 0–100.round(71.6 + 5 × Σ)clipped to 0–100 — Σ = the sum over relations ofgather+1 ·clash−1 ·heavyClash−2 ·listOnly0. One unit = 5 points, and 71.6 is the neutral point when there are no relations (the real-member average of the previous classic-text axis score, as of 2026-10-16). The scale is the same whether or not there is an hour pillar. The classic texts state only the direction and ranking of relations, not their size — this number is a scale assumption for display, not a score from the classic texts (KBR-SAJU-COMPAT-113).nullifrelationsisnullclassic1995(to be discontinued): still returned exactly as before. Planned schedule: announcement on 2026-11-16 → kept until 2026-12-31 →nullfrom 2027-01-01. Dates may change by service announcement. For new integrations userelationsandp2Score. Symbols from the male/female day-pillar intimacy table of a reference text (1995) (●대길 (great fortune) 100 ·◎중길 (good fortune) 90 ·○길 (fortune) 80 ·△무해무덕 (neutral) 70 ·×흉 (misfortune) 60 ·××대흉 (great misfortune) 50). The table is directional, male day pillar × female day pillar, so for same-gender pairs or when gender is not sent, both directions are read (A as male · B as male);scoreis the average of the two (e.g. 95), andsymbolandgradeare the grade of that average rounded down to the nearest 10 (e.g. 95 →◎중길). If both directions are the same, that symbol as is.conflictFrame:truewhen the symbol is favorable but the zodiac-animal relation is unfavorable, or the other way round. The whole object isnullif the day pillar cannot be foundastroSynastry: zodiac compatibility.layers= three-layer comparison of Sun · Moon · Ascendant;purposeBodies= priority planets by purpose (dating: Venus · Mars, marriage: + Saturn, friendship: Mercury · Jupiter …).relation=same_sign·opposite·same_element·element_support·same_modality·element_friction·neutral;textis its meaning (lang).score0–100.caveats/caveatTexts: notes when the Ascendant or Moon sign is estimated. If the time is unknown, the Ascendant layer is left outastroReadings: prewritten reading paragraphs for up to two of the most prominent contacts between the two people's charts (Sun–Moon, Venus–Mars, etc.). Empty array if none. Thelangof each paragraph is the language the text is actually in (koif there is no text in the requested language)headerBreakdown: overall scoreaverage(0–100) that combines five scores — six-axis average · authority (authority) · eight characters (full8) · classic-text share (classic—p2Scoresince 2026-10-16,classic1995.scorebefore) · zodiac (natal) — with per-purposeweights (weights). Missing scores have their weight redistributed among the rest. Same value as the headline score (big number) on the INTA app compatibility screen (62 in the example above). Since 2026-10-16, the weight of the classic-text share (classic) for the marriage purpose (marriage_stability) went from 0.25 to 0.05, and the freed 0.20 was split among the other four axes in their previous ratio (sixAxis0.15 → 0.19 ·authority0.25 → 0.3167 ·full80.25 → 0.3167 ·natal0.10 → 0.1267). Weights for other purposes are unchanged.nullif the day-pillar compatibility table entry cannot be found- The birth year must be 1900–2100 — otherwise 400
INVALID_BIRTH_YEAR(not charged). If a user types a two-digit year into a date field it goes in as e.g.0090, so block that on your screen too. If no result is found in the compatibility table, 400COMPAT_UNAVAILABLE(not charged) tti.tier: grade string from the zodiac-animal compatibility of a reference text (1995) (e.g. 대길 · 길 · 중길 · 보통 · 주의 · 흉 — great fortune to misfortune).tti.relation.type= the relation between the two animals' Earthly Branches — one of충(clash) ·원진(wonjin, resentment) ·형(punishment) ·해(harm) ·육합(six harmony) ·반합(half harmony) ·파(break) (if several overlap, the first in this order),nullif none.detailonly for half harmony (e.g. 수국(水局)).tti.relation.relations= all overlapping relations (same order, e.g. Ox · Goat["충","형"], empty array if none)tticontains only the animals, relation and grade. The zodiac-animal compatibility sentences of the reference text are not included (since 2026-10-11).categoryNarrative: 2–3 sentences on how the two people are in this relationship (purpose). The sentences are prewritten for every day-pillar combination (1,830 pairs) and each of the 6 relationship kinds, so the same input always gives the same text. In the text,나= A and상대= B (EnglishYou/Your partner, Japaneseあなた/相手, Thaiคุณ/คู่ของคุณ). Four languages vialang;nullif no text is found (charged the same)- Always included (from 2026-10-10):
saju.relationRatings={ key, stars }for five relationship kinds (keye.g. 연애 (dating), 결혼·동거 (marriage / living together), 친구 (friends), 사업 파트너 (business partner), 장기 관계 (long-term) / 1–5 stars) ·saju.meetingKeywords= three meeting keywords ·saju.authority= authority compatibility verdict (finalScore·grade·gradeTier·pattern·summary·levels(L1–L4) ·factors·flags·climateBalance) ·tti.conflictFrame= same value asclassic1995.conflictFrame yearFlow.years[].tier:great·good·neutral·caution.dayFlow.days[].tier:golden·good·neutral·dim·hard(dayFlow.golden/hardare the lists of those dates)
Relation kinds (relations[].kind)
kind | Meaning | tier |
|---|---|---|
yukhap | Branch six harmony — two branches pair up and combine | gather |
banhap | Branch half harmony — two of the three characters of a three-harmony set | listOnly |
samhap | Three harmony — combining the two charts brings three characters together to form one element frame | gather |
banghap | Directional combination — combining the two charts brings together three branches of the same season | gather |
chung | Branch clash — head-on collision | heavyClash |
hyung | Branch punishment | clash |
hae | Branch harm | clash |
pa | Branch break (the 寅亥 · 巳申 pairs are six harmonies, so listOnly) | clash |
wonjin | Wonjin (resentment) | listOnly |
stemHap | Stem combination between the two Day Masters | gather |
stemChung | Stem clash between the two Day Masters | clash |
napeumSaeng | The Nayin elements of the year pillars generate each other | gather |
napeumBihwa | The Nayin elements of the year pillars are the same | gather |
napeumGeuk | The Nayin elements of the year pillars control each other | clash |
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
- Basic data: both people's birth dates are required. Time and gender are optional. The intimacy symbol table is returned even without gender or for the same gender (both directions combined); the Ascendant in zodiac compatibility needs a birth time.
- What you get: zodiac compatibility uses the tropical zodiac. The relationship narrative comes in
categoryNarrativeand the zodiac paragraphs inastroReadings; if you need a longer custom reading, put the result into a/readingquestion. - Changed 2026-10-16 (
engineVersioncompat-2):relations·p2Score·engineVersionare new, andheaderBreakdown.classicbecamep2Score, soheaderBreakdown.averagemay differ from before (same value as the INTA app compatibility screen). Values of other existing fields are unchanged. - Compatibility: the fields added in 2026-10 (
classic1995·astroSynastry·astroReadings·headerBreakdown·lang, and on 2026-10-10saju.relationRatings·saju.meetingKeywords·saju.authority·tti.conflictFrame) are always added whether or not you sendlang. Values of existing fields are unchanged. However, since 2026-10-11 the fields that reproduced sentences from the reference text (1995) — the one-line zodiac-animal compatibility reading and the love and health fortune in the day-pillar dictionary — have been removed from the response. - Added 2026-10-27:
saju.iljuAxes(always, at the end of thesajublock) is new. The values and order of existing fields are unchanged, andengineVersionis stillcompat-2. - Changed 2026-10-31: Korean particles in compatibility sentences now agree with the final consonant (e.g. 「임와」 → 「임과」, 「병는」 → 「병은」). Keys, order, numbers and text in other languages are unchanged.
- Cost: ₩20 is deducted per call (including the added fields and the
details,flowand MBTI options; no extra charge).
Error codes
Besides the common errors (401 · 400 body validation · 402 · 429 · 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 400 | INVALID_BIRTH_YEAR | One person's birth year is outside 1900–2100 |
| 400 | INVALID_LUNAR_DATE | The lunar date does not exist |
| 400 | COMPAT_UNAVAILABLE | No compatibility data for this day-pillar combination |
| 402 | INSUFFICIENT_BALANCE | Insufficient 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
- Free version (free): 24 questions, ₩0 (free)
- Premium (premium): 93 questions, ₩10 per call
- An unknown
questionIdor anansweroutside 1–5 returns 400 and is not charged. - With
premium, if any axis has no answers at all, you get 400personality_axis_unansweredand are not charged. The response'saxeslists the empty axes (E_I·S_N·T_F·J_P).
Error codes
Besides the common errors (401 · 400 body validation · 402 · 429 · 503, etc.), this API returns:
| HTTP | Code | When |
|---|---|---|
| 400 | (none) | Questions — tier is not free or premium / Submit — unknown questionId |
| 400 | personality_axis_unanswered | Submit — premium, and an axis has no answers at all (empty axes in axes) |
| 402 | INSUFFICIENT_BALANCE | Submit — 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
| Parameter | Type | Required | Description |
|---|---|---|---|
birth | object | O | Same 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") |
sections | string[] | relations · structure · all. If omitted, everything (in the order relations → structure) | |
level | string | L1 (default) · L3. Items without an L3 explanation are filled with L1 and get fallbackLevel | |
lang | string | Only ko is supported | |
channel | string | stable (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": []
}
}
interpholds the explanation fields (meaningmeaning ·whyreasoning ·pitfallcaution). Each field is{text, reviewed, reviewedBy?, notes?}, and withstableonly reviewed fields are returned.unregistered: items that hold but have no explanation yet.pendingReview: items that have an explanation but are hidden instablebecause they are not reviewed yet.- Look up the original source of a rule card with
GET /kb/rules/{id}below.
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:
| HTTP | Code | When |
|---|---|---|
| 404 | kb_saju_explain_not_enabled | Account has not applied (all three paths) |
| 400 | INVALID_LUNAR_DATE · INVALID_LOCATION | explain — the lunar date does not exist / only one of latitude and longitude was sent |
| 403 | KB_PREVIEW_NOT_ALLOWED | explain — account not allowed to use channel: "preview" (not charged) |
| 400 | KB_DOMAIN_UNKNOWN | release — unsupported domain |
| 404 | KB_RULE_NOT_FOUND | rules — unknown rule id |
| 503 | KB_NOT_LOADED | The 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.
- The sender address is the RIDM Apps mail sender address, and the sender name is the app name (you can change it in console > My apps > Mail) followed by "(RIDM Apps)" (e.g.
Moonlight Bakery (RIDM Apps)). Names of banks, card and payment brands, public institutions (tax office, police, prosecutors, government, post office, etc.) and names like HRUM or RIDM cannot be used as the sender name. Replies go toreplyToor the address set in the console. - Only partners with an email-verified console account can send. Before verification the API returns 403
MAIL_EMAIL_UNVERIFIED. - Advertising email cannot be sent. A subject starting with
(광고)("(Ad)") is rejected with 400. Commercial advertising such as discount or event notices requires the recipient's prior consent and an opt-out method under Article 50 of the Korean Information and Communications Network Act, so do not send it through this API (Terms of Service Article 20-2). - One recipient counts as one email. You can put up to 10 recipients in one request; each is sent separately, so recipients cannot see each other's addresses.
- Addresses that bounced (nonexistent address) or reported spam are not sent to afterwards and are answered with
suppressed. These are not counted and not charged. - Email kind (
kind): the default istransactional(notices that must be sent, such as sign-up, orders and passwords) and is sent as it is today. Send app notifications the recipient can turn off (new comments, booking reminders, etc.) with"kind": "notification". These emails carry one-click unsubscribe headers (List-Unsubscribe,List-Unsubscribe-Post, RFC 8058), so mail apps such as Gmail can show an "Unsubscribe" button. For an unsubscribed address, only that app'snotificationemails are skipped and answered withsuppressed(not counted, not charged).transactionalemails to the same address and emails from other apps still go out.notificationis not advertising email either — advertising cannot be sent with any kind. Unsubscribe records are kept for 3 years (1,095 days); after that they are deleted andnotificationemails may reach that address again. - Automatic suspension: if over the last 30 days spam reports reach 0.1% of emails sent (at least 2) or permanent bounces reach 5% (at least 10) (if fewer than 200 were sent, it is calculated as 200), mail sending for that app is stopped immediately and you are notified. The same applies when impersonation or phishing is suspected. The app itself keeps running; while stopped, the API returns 403
MAIL_SUSPENDED. To lift it, contact contact@float.do (Terms of Service Article 20-2).
POST /api/v1/partners/v1/mail/send
Request parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
to | string | string[] | O | 1–10 recipient addresses. Addresses with a display name (Name <address>) are not accepted |
subject | string | O | Subject, 1–200 characters, no line breaks |
text | string | △ | Body (plain text). Either text or html is required. Up to 100,000 characters |
html | string | △ | 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 |
replyTo | string | Reply-to address. If absent, the address set in the console; if that is also absent, sent without a reply-to address | |
attachments | object[] | Up to 3 attachments, 512KB in total. { "filename", "contentType", "content" } (content is base64). Only PDF · PNG · JPEG · GIF · WebP · TXT · CSV · ICS are accepted | |
kind | string | transactional (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
| Item | Details |
|---|---|
| Monthly included emails | Included 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 emails | On 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 limit | 500 per app by default (1–5,000 in the console). Resets at midnight Korean time |
| Per-minute limit | 30 per minute per app |
Errors
| HTTP | code | Description |
|---|---|---|
| 400 | MAIL_INVALID | Invalid 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.) |
| 402 | insufficient_credit | Not enough credits to pay for extra sending. Nothing is sent |
| 403 | MAIL_APP_KEY_REQUIRED | Called with a key that is not an app key |
| 403 | APP_NOT_ACTIVE | The app linked to the key is not running |
| 402 | MAIL_OVERAGE_UNPAID | Unpaid extra email charges exceed the balance. Top up and the outstanding charges are deducted and sending reopens (sending within the included emails keeps working) |
| 403 | MAIL_EMAIL_UNVERIFIED | No email-verified console account |
| 403 | MAIL_SUSPENDED | Mail sending for this app is suspended because spam report or bounce thresholds were exceeded, or an operator stopped it |
| 429 | MAIL_RATE_LIMITED | Per-minute limit exceeded. Send again after retryAfterSeconds |
| 429 | MAIL_DAILY_CAP_EXCEEDED | Daily limit exceeded. Reopens at midnight Korean time |
| 429 | MAIL_MONTHLY_LIMIT | This month's included emails are used up (free plan, or extra sending is off) |
| 503 | MAIL_UNAVAILABLE | The 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.
- Allowed origins: only signals from the addresses entered at registration (https, no path, up to 5) are counted. The domain of the site address must also be among the allowed origins.
- How counting works: each tab sends a signal every 30 seconds, and tabs that sent a signal in the last 75 seconds are counted as "online". When a tab is hidden it stops signaling; when it is closed it drops out right away.
- Inflation protection: signals from the same tab are counted once (repeats within 10 seconds are ignored), and there are caps on how many tabs one IP can add and on signals per site. Signals with an empty User-Agent, from automation tools or headless browsers are not counted.
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.
- The site key (
pk_presence_…) is a public key, so it is fine to put it in your page source. Even if another site takes it, signals are not counted unless they come from an allowed origin. - When the tab is not visible (switching to another tab or minimizing the app), signals stop, and one is sent right away when it becomes visible again. When the tab is closed or left, one leave signal is sent; even if that signal does not get through, the tab drops out after 75 seconds.
- Reloading in the same tab still counts as one person. Each tab creates a random session value and keeps it only in that tab's
sessionStorage. No cookies or login data are sent. - If the key is wrong or the network is blocked, no error is raised; the number just stays empty. Even if the script is added twice (tag manager + direct embed, etc.), only the one that started first is used, so nothing is counted twice.
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 · path | Description |
|---|---|
POST /api/v1/partners/v1/presence/beat | Send 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/leave | Removes 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}/online | Get 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}/count | Send 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}/rooms | Per-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/directory | List 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.
POST /api/v1/partners/v1/presence/sites/{siteKey}/count· body{ "online": integer }(0–100,000) · headerX-Presence-Secret: sk_presence_…orAuthorization: Bearer sk_presence_…. Response{ online, reportedAt }- It is server-to-server only, so browser CORS is not open. Sending with the public key (
pk_presence_…) returns 401. - Accepted up to once per minute per site. Sending more often returns 429 with
Retry-After(seconds). - If your server sent a value in the last 5 minutes, that value is used as the visitor count (
source: "server"); after 5 minutes it falls back to aggregated script signals (source: "beacon"). With no signals either, it is 0. - If the value suddenly jumps far above the previous one, it is still shown as is but logged for operational review.
- Put only
onlineand theroomsbelow in the body. Any other field returns 400.
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" } }
roomsholds up to 200 entries.idis 1–64 characters of letters, digits,_,-(no#or spaces) and must not repeat within one body.nis an integer 0–100,000 andcapan integer 1–100,000. Any other field in a room object returns 400.- If
nis greater thancap, you get 400PRESENCE_ROOM_OVER_CAP. Not accepting people beyond capacity is the room server's job, so overflowing values are not stored. Other format errors return 400PRESENCE_BAD_REQUESTwithfield(e.g.rooms[3].id). On a 400, the total number is not stored either. - Each report replaces the whole room list. Rooms missing from the next report disappear, and a report without
roomsis treated as having no rooms. - If there is no new report for 2 minutes after the last one, the room list reads as an empty list (
[]). This is separate from the 5-minute rule for the total. Reporting every minute keeps it from dropping. - A report rejected by the once-per-minute limit (429) does not change the room list either.
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"
} }
- The headers are the same as for
count(X-Presence-SecretorAuthorization: Bearer). Only that site's server key is accepted — a public key, another site's server key or no key returns 401, and a revoked site returns 404. roomscome in numeric order ofid(studio-2beforestudio-10), andfree=cap − n. To fill lower-numbered rooms first, pick the first room withfree > 0.reportedAtis the time of the last report received (nullif there has been none). Even when the room list is empty after 2 minutes, this value tells you when the last report came in.- It is server-to-server only, so browser CORS is not open and responses are not cached (
Cache-Control: no-store). Up to 60 calls per minute per site; beyond that, 429 withRetry-After. - Only room ids and count/capacity numbers are accepted. Do not include user information.
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
- Visitors' IP addresses and User-Agents are not stored. IPs are used briefly in memory only for abuse-prevention calculations and are not logged.
- The server keeps only a session hash
sha256(siteKey + ":" + sid)and the time of the last signal, and deletes them after 10 minutes. No cookies are used. - "Online" is the number of tabs that sent a signal in the last 75 seconds.
Errors
| HTTP | code | Description |
|---|---|---|
| 400 | PRESENCE_BAD_REQUEST · INVALID_BODY | Invalid siteKey or sid format, or the body is not JSON |
| 401 | PRESENCE_SECRET_REQUIRED | Server key missing or wrong (including sending the public key) — count only |
| 403 | PRESENCE_ORIGIN_NOT_ALLOWED | Origin missing or not an allowed origin |
| 404 | PRESENCE_SITE_NOT_FOUND | Unknown or revoked key |
| 413 | PAYLOAD_TOO_LARGE | Body exceeds 1KB |
| 429 | PRESENCE_RATE_LIMITED | Too 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
- A dedicated token used by the AI (starts with
hrum_cm_). Send it in the request headerAuthorization: Bearer hrum_cm_…. - The default permission is read-only (account · key list · credits · usage · hosting · payout status). The four write permissions —
keys(issue, reissue and revoke API keys) ·credit(top-up links, refund preview) ·hosting(apply for and cancel hosting) ·seller(seller registration link) — are checked one by one by a person on the issue consent screen. hosting_ops(AI operations token — set and delete environment variables of your hosted apps, start deploys and view deploy status, roll back and restart, view runtime logs) is also checked on the issue consent screen. This permission does not ask for consent again on each action. How to set environment variables and secrets and which names the AI path allows is in RIDM Apps deploy guide — set secrets with AI; how to start deploys and view deploy status and logs is in Deploy and view logs with AI.- Tokens are valid for 30 days. Each account can have up to 10 live tokens.
- In the "AI console management tokens" list on the console API keys screen you can see each token's prefix, permissions and last use time, and revoke it instantly at any time. The token value itself is never shown again.
Consent flow
- The AI calls
POST /api/v1/partners/v1/console-ai/consentswith what it wants to do (what · amount · target) and gets aconsentUrland acode. - The AI shows the link to the user (MCP can also open it in the browser).
- 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.
- The AI checks the approval with
GET /console-ai/consents/{code}, then continues by puttingconsentCodein 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 · path | What it does | Permission | Consent |
|---|---|---|---|
POST /consents | Create a consent request ({action, payload}) | The action's permission (none for the first token) | — |
GET /consents/{code} | Consent status: pending · approved · denied · expired · used | read (poll secret for the first token) | — |
GET /me · GET /token | My account · this token's permissions | read | — |
GET /status | Getting-started checklist (API · hosting · payment status and next steps) | read | — |
GET /keys | API key list (prefixes only) | read | — |
GET /credit · GET /credit/transactions | Credit balance · history (offset, limit ≤ 100) | read | — |
GET /usage | Calls and spending by period (from, to) | read | — |
GET /hosting | Hosting eligibility · plan · app status | read | — |
GET /seller · GET /payouts | Seller status (excluding personal identity data) · payout history | read | — |
POST /keys | Issue an API key ({label?}) | keys | key_issue |
POST /keys/{id}/rotate | Issue a new key + turn this key off immediately | keys | key_rotate |
POST /keys/{id}/revoke | Revoke a key (cannot be undone) | keys | key_revoke |
POST /credit/topup-link | Top-up payment link ({planKey}) | credit | credit_topup_link |
POST /credit/refund-preview | Calculate the refundable amount (does not request a refund) | credit | credit_refund_preview |
POST /hosting/apply | Apply for hosting ({planKey, subdomain, recurringConsent?}) | hosting | hosting_apply |
POST /hosting/apps/{id}/cancel | Cancel hosting | hosting | hosting_cancel |
POST /seller/apply-link | Link to the seller registration screen | seller | seller_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
| Code | HTTP | Meaning |
|---|---|---|
MGMT_TOKEN_INVALID | 401 | Token missing, expired or revoked. Get a new one |
MGMT_SCOPE_DENIED | 403 | This token lacks the required write permission (scope field). Issue a new one with that permission checked |
ACCOUNT_SUSPENDED | 403 | Suspended account |
CONSENT_REQUIRED | 400 | No consentCode in the write request |
CONSENT_PENDING | 409 | The person has not agreed yet |
CONSENT_DENIED | 403 | The person declined |
CONSENT_EXPIRED | 410 | Expired after 10 minutes |
CONSENT_USED | 409 | Consent code already used |
CONSENT_MISMATCH | 409 | The current request differs from what was agreed (amount · target) |
CONSENT_NOT_FOUND | 404 | Unknown code, or a code from another account |
INVALID_CONSENT_PAYLOAD | 400 | Invalid payload value (field field) |
UNKNOWN_FIELD | 400 | Contains a field the AI path does not accept |
CONSENT_RATE_LIMITED | 429 | Too many consent requests |
MGMT_TOKEN_LIMIT | 409 | 10 live tokens already, so no more can be issued |
NOT_AVAILABLE | 404 | Menu 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
| Tool | What it does |
|---|---|
hrum_ai_connect · hrum_ai_connect_status | Create 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_list | Lookups |
hrum_consent_request · hrum_consent_status | Request write consent · check status |
hrum_key_issue · hrum_key_rotate · hrum_key_revoke | Issue and reissue keys (new keys are saved only to .env.local) · revoke |
hrum_topup_link · hrum_refund_preview | Top-up link · refundable amount |
hrum_hosting_apply · hrum_hosting_cancel · hrum_seller_apply_link | Apply 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
| HTTP | Code | When |
|---|---|---|
| 429 | GUEST_QUOTA_EXCEEDED | More than 5 calls this month from the same IP (combined with the natal chart guest endpoint) |
| 400 | INVALID_LUNAR_DATE · INVALID_LOCATION | The 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).
- Manseryeok —
wonkuk.*.stem·branch·ohaeng, thestem·branch·ohaengofdaeun[]·seun[], the keys ofohaengBalance,cheonganDetail.seasonal.monthBranch, the keys ofyongshinDetail.element_states,byeongyak.primary·tonggwan.primary(element codes — display name inprimaryLabel), 「청」 (clear) · 「탁」 (murky) injeongtak.verdict(only 「판정불가」 (undetermined) is translated), the keys ofglossary - Manseryeok
identity· Saju Identity —saju.{year,month,day,hour}.{cheongan,jiji,ohaeng}, thestem·branch·ohaeng·ganjioftimeline(current · daeunChain · years · highlights),gongmang.void_branches[]·gongmang.hits[].jiji,samjae.current_year_branch,season_strength_hint.yongshin_hint.primary[]·secondary[],sinsal_list[].id(e.g.sinsal12_재살), the keys ofyongshin.element_states,sipsin_map.* - Manseryeok
astrology: true—natalChart.bodies[].sign·element(English insignEn) — same notation as the natal chart API - Today's Saju —
summary.day_master(e.g. 「계」 Gye),summary.ilju(「계미」 Gye-mi),summary.today_day_ohaeng,summary.yongshin,omen.color.ohaeng - Fortune Flow — stem-branch readings, element and relation names,
chars[].sipsin,natal.level·natal.result(Day Master strength verdict, e.g. 「약약」 (weak) · 「중화」 (balanced)),natal.primaryYongshin·natal.shin.*,triples[].element,compatInputs.*(including the keys ofsurfaceElementCounts) - Compatibility —
saju.personA/B.ilju,dictionary.a/b.id(ilju_경진) ·name_kr, thestem·branch·ohaeng·relations[].target·typeofyearFlow.years[].a/b, the keys ofsaju.full8.personA/B.elementDistribution·sipsinCount·combinedElementBalance.distribution
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.
- 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
- Service protection cap — per-minute cap on all requests from the same IP (429, no
code) - API key —
X-Partner-Api-Keycheck (401) - Feature enabled — a feature not enabled for the account (404
*_not_enabled) - Request limits — per-second and daily limits set in the console (429
RATE_LIMIT_EXCEEDED) - Body validation — field names, types, ranges (400)
- 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 strings | No |
| 401 | (none) | X-Partner-Api-Key missing, invalid key, revoked key, inactive account | No |
| 402 | INSUFFICIENT_BALANCE | Insufficient credit balance. In streaming responses, event: error carries the same code and status | No |
| 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 balance | No |
| 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 Quickstart | No |
| 404 | *_not_enabled | A 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 minute | No |
| 429 | RATE_LIMIT_EXCEEDED | Per-second or daily limit set in the console exceeded. The body has retryAfterSeconds and there is a Retry-After header | No |
| 500 | (none) | Server error. Any amount already deducted for this request is returned after the response | Refunded |
| 503 | SERVICE_DEGRADED | Temporary database connection problem. Retry-After: 30 — try again after 30 seconds | Refunded |
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.
| Code | Meaning | Fields |
|---|---|---|
UNKNOWN_TIMEZONE | The 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 empty | timezone 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.
- When limits are set, responses carry the
X-RateLimit-Limit-Second/X-RateLimit-Remaining-Second/X-RateLimit-Limit-Day/X-RateLimit-Remaining-Dayheaders. - Exceeding a limit set by the partner returns
429+{"code":"RATE_LIMIT_EXCEEDED","retryAfterSeconds":N}and aRetry-Afterheader. Blocked requests are not charged. - Guests (no key) are fixed at 5 free test calls per month.
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
- Manseryeok and natal chart guest endpoints: 5 free calls per month combined (limited by IP)
- Test in the Playground without logging in
- The remaining count is included in the response (guestRemaining)
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.
| Inquiry | Contact |
|---|---|
| Technical support · outage reports · partnerships | contact@float.do |
Sending the following along with your inquiry helps us check faster.
- The endpoint you called and the time (including time zone)
- The first 12 characters of your API key (including
float_live_; never send the full key) - The HTTP status code and body of the response you got
For terms and personal data handling, see the Terms of Service (Korean) and the Privacy Policy (Korean).