{
  "openapi": "3.0.3",
  "info": {
    "title": "EPU4 SDK API",
    "version": "4.0.0",
    "description": "Interactive API reference for the EPU SDK, based on the **EPU SDK - API Integration Guide** (October 19, 2026).\n\n**Workflow:** log in with your EPU secret and device ID using `POST /user_login`; pass the returned Bearer token to `POST /derive` and `GET /mdad`. To update MDAD, send **only `text`** in a `/derive` request. This updates the EPU without an OpenAI call, and broadcasts an updated `mdad` event to connected Socket.IO clients. Add optional `reasoning: true` to request an AI explanation or `action: true` for a suggested robot action.\n\n**Realtime updates:** connect to the Socket.IO namespace `https://emohuman.com/<epuid>` and listen for `mdad`. `GET /mdad` waits for the next MDAD frame (within approximately five seconds) and returns an MDAD numeric array. The October 19 guide describes 120 values; a live EPU4 REST read on October 31, 2026 returned 123. Check the actual length.\n\n**Current gateway session policy:** by default, the Bearer session expires after 60 minutes without an authenticated REST API request; each authenticated REST request renews the inactivity timer. An optional login `duration` (minutes) changes this default. Receiving Socket.IO MDAD events does not renew the REST session. The Bearer token has a separate maximum validity of 24 hours. An opt-in `GET /mdad` around 55 minutes of inactivity can keep an intentional long-running session active, but can consume an HTTP cached MDAD frame; never enable keepalive silently for abandoned clients. If a protected request returns HTTP 401, obtain a new token. Socket.IO may remain connected without a valid REST session. See `x-metasoul-session` below.\n\n**Source limitations:** the original integration guide does not specify detailed HTTP error status codes, token expiry, CORS policy, or the semantic meaning of individual MDAD values. The session policy above describes the deployed gateway implementation, not guarantees of that original guide.\n\n**MDAD decoding:** The companion `mdad-channels.json` and the `x-metasoul-mdad` extension below document the 12 apex indexes and Optimistic (index 9) from the *EPU-III Brochure*, pp. 14-15. This is separate from the EPU4 integration guide, which specifies the array length but does not individually define its channels. A live EPU4 response contained indexes 120-122 (123 total values), but index 123 was absent. The legacy interpretation of those extra indexes is not yet verified against EPU4 firmware.\n\n**Agent instructions:** https://emohuman.com/docs/EPU4/agent-guide.md"
  },
  "servers": [
    {
      "url": "https://emohuman.com",
      "description": "EPU4 service"
    }
  ],
  "tags": [
    {
      "name": "Authentication",
      "description": "Exchange EPU device credentials for a session token."
    },
    {
      "name": "Emotion processing",
      "description": "Send text to update MDAD, optionally request AI reasoning or a robot action."
    },
    {
      "name": "MDAD",
      "description": "Read the latest streamed numeric MDAD array (at least 120; 123 observed live)."
    },
    {
      "name": "Account inventory",
      "description": "Read-only account EPUID listing using one account secret; no EPU login or device operation."
    }
  ],
  "paths": {
    "/user_login": {
      "post": {
        "tags": [
          "Authentication"
        ],
        "summary": "Log in and obtain a session token",
        "operationId": "userLogin",
        "description": "The bundled EPU4 documentation has a **Connect to EPU4** panel that calls this endpoint and automatically applies the returned Bearer token to protected requests. In other OpenAPI viewers, call this endpoint and enter the returned `token` using **Authorize**. The returned `epuid` identifies the Socket.IO namespace. The current gateway defaults to 60 minutes of REST inactivity; optional `duration` specifies a different timeout in minutes. Token validity is separately capped at 24 hours.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              },
              "example": {
                "secret": "YOUR_EPU_SECRET",
                "epuid": "YOUR_EPU_ID"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful login (example from integration guide).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginSuccess"
                },
                "example": {
                  "reply": "ok",
                  "token": "abc123...",
                  "epuid": "your_epu_id"
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/UnspecifiedError"
          }
        }
      }
    },
    "/derive": {
      "post": {
        "tags": [
          "Emotion processing"
        ],
        "summary": "Update MDAD from text (optional AI reasoning or action)",
        "operationId": "deriveEmotion",
        "description": "Send a situation or message with **`text` only** to make the EPU process it and update the MDAD **without an OpenAI call**. Every `/derive` call broadcasts an updated MDAD frame to connected Socket.IO clients. Both `reasoning` and `action` are **optional**. Set `reasoning: true` if you want an AI-generated explanation, or `action: true` for a suggested robot action. A text-only request normally responds with `{\"reply\":\"ok\"}`; no AI-generated `text` field is required.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeriveRequest"
              },
              "examples": {
                "send": {
                  "summary": "Send text to update MDAD (no AI call)",
                  "value": {
                    "text": "I am walking alone at night in a dark street"
                  }
                },
                "reasoning": {
                  "summary": "Explain emotion result",
                  "value": {
                    "text": "I am walking alone at night in a dark street",
                    "reasoning": true
                  }
                },
                "action": {
                  "summary": "Suggest robot action",
                  "value": {
                    "text": "I am walking alone at night in a dark street",
                    "action": true
                  }
                },
                "timezone": {
                  "summary": "Reasoning with timezone",
                  "value": {
                    "text": "I am walking alone at night in a dark street",
                    "reasoning": true,
                    "timezone": "America/New_York"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Successful request: text-only mode returns reply ok; optional AI modes can also include generated text.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeriveSuccess"
                },
                "examples": {
                  "send": {
                    "summary": "Text-only MDAD update",
                    "value": {
                      "reply": "ok"
                    }
                  },
                  "reasoning": {
                    "summary": "Reasoning result",
                    "value": {
                      "reply": "ok",
                      "text": "I chose Fear because walking alone at night is a life-threatening situation..."
                    }
                  },
                  "action": {
                    "summary": "Robot action result",
                    "value": {
                      "reply": "ok",
                      "text": "First action would be to scan surroundings and find a safe exit route"
                    }
                  }
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/UnspecifiedError"
          }
        }
      }
    },
    "/mdad": {
      "get": {
        "tags": [
          "MDAD"
        ],
        "summary": "Receive the next MDAD array",
        "operationId": "getMdad",
        "description": "Retrieves MDAD for the authenticated EPU. The endpoint has **no request body**. Per the guide, it waits for the next MDAD streamed in from the EPU (within approximately **5 seconds**), then returns a numeric MDAD array. The 2026 source guide describes 120 values; a live REST response contained 123 on October 31, 2026. Validate the actual length. An authenticated GET renews the gateway's inactivity timer, so an opt-in keepalive may call GET /mdad after 55 minutes without other authenticated REST activity. The call can consume a cached HTTP frame; it does not generate an emotion or consume the Socket.IO subscription. A 503 EPU_NOT_CONNECTED means no new MDAD frame was available and does not by itself indicate an expired token. The original integration guide did not specify timeout response details.",
        "security": [
          {
            "BearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Numeric MDAD array: at least 120 values; a live response contained 123.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MdadSuccess"
                },
                "example": {
                  "reply": "ok",
                  "mdad": [
                    0,
                    0,
                    1,
                    1,
                    2,
                    1,
                    1,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    0,
                    51,
                    49,
                    50
                  ]
                }
              }
            }
          },
          "default": {
            "$ref": "#/components/responses/UnspecifiedError"
          }
        }
      }
    },
    "/epuid_list": {
      "post": {
        "tags": [
          "Account inventory"
        ],
        "operationId": "listAccountEpuids",
        "summary": "EPUID List for one account secret",
        "description": "Read-only account inventory. Send a single MetaSoul account secret, without an EPUID, Bearer token or device login. The gateway calls its existing `POST /get_secret_info` integration once and returns **only** validated EPUID lists grouped as `available`, `in_use` and `availability_unknown`. No raw account response, account metadata or secret is returned. The upstream nested `avl`/`resrved` grouping comes from the existing population-initialization client; it is not an independently verified live availability or reservation contract. A flat response is explicitly classified `availability_unknown`. The endpoint does not select, reserve, log in to, initialize or operate an EPU. Full identifiers and account membership are sensitive; avoid logging responses. Requires server route registration, rate limiting, and an explicit `EPUID_LIST_ENABLED=1` deployment setting. Do not use until deployed; HTTP 503 means the feature is disabled.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountSecretRequest"
              },
              "example": {
                "secret": "YOUR_ACCOUNT_SECRET"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Validated account EPUID inventory with no raw upstream fields.",
            "headers": {
              "Cache-Control": {
                "description": "no-store",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountEpuidListResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing, malformed or unexpected request fields."
          },
          "502": {
            "description": "Upstream account inventory unavailable, rejected, ambiguous or invalid."
          },
          "503": {
            "description": "EPUID listing is disabled on the gateway."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "In the bundled EPU4 interactive docs, the Connect panel fills this automatically. For other OpenAPI viewers, use the `token` returned by `POST /user_login` (without the `Bearer` prefix). The default gateway inactivity limit is 60 minutes; Socket.IO MDAD frames do not extend it. The token also has a maximum lifetime of 24 hours."
      }
    },
    "schemas": {
      "LoginRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "secret",
          "epuid"
        ],
        "properties": {
          "secret": {
            "type": "string",
            "minLength": 1,
            "description": "Your EPU secret key.",
            "example": "YOUR_EPU_SECRET"
          },
          "epuid": {
            "type": "string",
            "minLength": 1,
            "description": "Your EPU device ID.",
            "example": "YOUR_EPU_ID"
          },
          "duration": {
            "type": "number",
            "minimum": 0,
            "exclusiveMinimum": true,
            "description": "Optional REST inactivity timeout in minutes; defaults to 60 if omitted.",
            "example": 60
          }
        }
      },
      "LoginSuccess": {
        "type": "object",
        "required": [
          "reply",
          "token",
          "epuid"
        ],
        "properties": {
          "reply": {
            "type": "string",
            "example": "ok",
            "description": "`ok` on success."
          },
          "token": {
            "type": "string",
            "description": "Use this value in the Authorization Bearer header. The JWT has a 24-hour maximum lifetime even if authenticated REST activity renews the idle session.",
            "example": "abc123..."
          },
          "epuid": {
            "type": "string",
            "description": "EPU device ID returned by the server."
          }
        }
      },
      "DeriveRequest": {
        "type": "object",
        "required": [
          "text"
        ],
        "description": "Only `text` is required. Omit `reasoning` and `action` to update MDAD without an AI-generated response; set either to `true` only when needed.",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "description": "Situation or message to analyze.",
            "example": "I am walking alone at night in a dark street"
          },
          "reasoning": {
            "type": "boolean",
            "description": "Optional. Set true to request an AI-generated explanation; omit for a text-only MDAD update."
          },
          "action": {
            "type": "boolean",
            "description": "Optional. Set true to request a suggested robot action; omit for a text-only MDAD update."
          },
          "timezone": {
            "type": "string",
            "description": "Optional client timezone, e.g. `America/New_York`.",
            "example": "America/New_York"
          }
        }
      },
      "DeriveSuccess": {
        "type": "object",
        "required": [
          "reply"
        ],
        "properties": {
          "reply": {
            "type": "string",
            "example": "ok",
            "description": "`ok` on success."
          },
          "text": {
            "type": "string",
            "description": "Optional AI-generated explanation or action; not present for a text-only MDAD update."
          }
        }
      },
      "MdadSuccess": {
        "type": "object",
        "required": [
          "reply",
          "mdad"
        ],
        "properties": {
          "reply": {
            "type": "string",
            "example": "ok",
            "description": "`ok` on success."
          },
          "mdad": {
            "type": "array",
            "minItems": 120,
            "description": "Numeric MDAD array. The October 19, 2026 EPU4 guide describes 120 values, but a live October 31 EPU4 GET /mdad returned 123; do not enforce an exact length. The first 120 contain the historical emotion/feeling layout (APEX indexes 4,14,...,114; Optimistic 9). Indexes 120-122 were present in that live response; their EPU-III meanings are not yet independently confirmed on EPU4. Index 123 was absent.",
            "items": {
              "type": "number"
            }
          }
        }
      },
      "ErrorReply": {
        "type": "object",
        "description": "Only `reply: \"error\"` is indicated in the guide; any further fields and HTTP status are unspecified.",
        "properties": {
          "reply": {
            "type": "string",
            "example": "error"
          }
        },
        "additionalProperties": true
      },
      "AccountSecretRequest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "secret"
        ],
        "properties": {
          "secret": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096,
            "writeOnly": true,
            "format": "password",
            "description": "Account-wide MetaSoul secret; use HTTPS and never record or print this value.",
            "example": "YOUR_ACCOUNT_SECRET"
          }
        }
      },
      "AccountEpuidListResponse": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "reply",
          "total",
          "available",
          "in_use",
          "availability_unknown"
        ],
        "properties": {
          "reply": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "total": {
            "type": "integer",
            "minimum": 1,
            "description": "Total distinct EPUIDs in this account inventory."
          },
          "available": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-F0-9]{32}$"
            },
            "description": "EPUIDs listed in the upstream avl group."
          },
          "in_use": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-F0-9]{32}$"
            },
            "description": "EPUIDs listed in the upstream resrved/reserved group."
          },
          "availability_unknown": {
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^[A-F0-9]{32}$"
            },
            "description": "EPUIDs from flat legacy inventory responses with no reliable status grouping."
          }
        }
      }
    },
    "responses": {
      "UnspecifiedError": {
        "description": "Error (the integration guide does not specify HTTP error statuses or additional fields).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorReply"
            },
            "example": {
              "reply": "error"
            }
          }
        }
      }
    }
  },
  "externalDocs": {
    "description": "MetaSoul EPU4 bilingual interactive docs and live MDAD monitor",
    "url": "https://emohuman.com/docs/EPU4/"
  },
  "x-metasoul-mdad": {
    "arrayIndexing": "zero-based",
    "documentedEPU4ArrayLength": 120,
    "layoutSource": "EPU-III Brochure, pages 14-15 (distinct from EPU4 API guide)",
    "apexIndexFormula": "4 + 10 * emotionIndex",
    "emotionApexChannels": [
      {
        "emotion": "Excite",
        "channel": 4
      },
      {
        "emotion": "Confident",
        "channel": 14
      },
      {
        "emotion": "Happy",
        "channel": 24
      },
      {
        "emotion": "Trust",
        "channel": 34
      },
      {
        "emotion": "Desire",
        "channel": 44
      },
      {
        "emotion": "Fear",
        "channel": 54
      },
      {
        "emotion": "Surprise",
        "channel": 64
      },
      {
        "emotion": "Inattention",
        "channel": 74
      },
      {
        "emotion": "Sad",
        "channel": 84
      },
      {
        "emotion": "Nostalgia",
        "channel": 94
      },
      {
        "emotion": "Disgust",
        "channel": 104
      },
      {
        "emotion": "Anger",
        "channel": 114
      }
    ],
    "optimisticFeelingChannel": 9,
    "legacyAdditionalChannels": [
      {
        "name": "Pulse speed",
        "channel": 120,
        "interpretation": "0 slower; 50 normal; 100 faster"
      },
      {
        "name": "Frustration / Satisfaction",
        "channel": 121,
        "interpretation": "0 maximum satisfaction / relief of frustration; 50 neutral; 100 maximum frustration"
      },
      {
        "name": "Pain / Pleasure",
        "channel": 122,
        "interpretation": "0 maximum pleasure / relief of pain; 50 neutral; 100 maximum pain"
      },
      {
        "name": "Temperature",
        "channel": 123,
        "interpretation": "EPU-III brochure lists a range of -40 to +85 °C"
      }
    ],
    "warning": "The October 19 source guide documented 120 values, but one live EPU4 REST read on October 31 returned 123. Validate the actual payload length. Indexes 120-122 were present in that read; their EPU-III interpretations are not yet verified for EPU4. Index 123 (historical temperature) was absent.",
    "fullChannelMap": "https://emohuman.com/docs/EPU4/mdad-channels.json",
    "observedLiveEPU4ArrayLengths": [
      123
    ],
    "observationDate": "2026-10-31"
  },
  "x-metasoul-socket-io": {
    "type": "Socket.IO",
    "connectionBaseUrl": "https://emohuman.com",
    "namespaceTemplate": "/{epuid}",
    "epuidSource": "Use epuid returned by POST /user_login",
    "receiveEvent": "mdad",
    "payload": {
      "epuid": "string",
      "mdad": "number[] (at least 120; 123 observed via REST)"
    },
    "trigger": "A POST /derive request, including a text-only request, triggers a broadcast",
    "security": "The integration guide does not describe Socket.IO authentication; do not assume REST Bearer token is needed for the socket.",
    "reference": "https://emohuman.com/docs/EPU4/agent-guide.md"
  },
  "x-metasoul-session": {
    "scope": "Describes the current gateway implementation; original integration guide did not specify token policy.",
    "defaultInactivityTimeoutMinutes": 60,
    "configurableLoginDurationMinutes": true,
    "maximumJwtLifetimeHours": 24,
    "inactivityRenewal": "An authenticated REST API request refreshes the session's last-used time.",
    "socketIoEventsRenewInactivity": false,
    "optionalKeepalive": {
      "method": "GET",
      "path": "/mdad",
      "afterMinutesWithoutRestActivity": 55,
      "automaticByDefault": false,
      "caution": "GET /mdad can consume an HTTP cached frame or wait for a new one. Do not keep abandoned EPUIDs reserved."
    },
    "sessionFailure": "HTTP 401 means the REST session is invalid or expired; obtain a new token.",
    "noMdadFrame": "HTTP 503 EPU_NOT_CONNECTED can occur after successful authentication and is not a token expiry.",
    "disconnectCaution": "The Swagger Disconnect button clears browser state but does not immediately release the upstream EPU reservation."
  }
}
