> ## Documentation Index
> Fetch the complete documentation index at: https://ormaprotocol.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Orma REST API: Endpoints, Contracts, and Error Codes

> The eleven GET endpoints Orma exposes, the six response contracts that bind every route, and the error codes and HTTP statuses returned on failure.

Orma exposes eleven `GET` endpoints over plain HTTP — no authentication, no framework, no middleware stack. The server defaults to port `8787` and is overridable with the `PORT` environment variable. Every response you receive follows the six contracts below without exception; read them once and you will not be surprised by any individual route.

## Base URL

```
http://localhost:8787
```

Orma is self-hosted. There is no hosted cloud URL. The service runs against XRPL Devnet because XLS-65 vaults and XLS-66 lending are not yet amended into Mainnet.

<Warning>
  All data comes from XRPL Devnet. The `network` field is hard-coded to `"devnet"` in every response. `health.source` tells you whether the data came from a live Devnet read (`"devnet"`) or from the recorded fixture ring (`"fixtures"`).
</Warning>

## Route Index

| Method | Path                                  | Answers                                                          |
| ------ | ------------------------------------- | ---------------------------------------------------------------- |
| `GET`  | `/api/health`                         | Is the reader connected, and how old is the data                 |
| `GET`  | `/api/vaults`                         | Which facilities are tracked, worst first                        |
| `GET`  | `/api/vaults/:vaultId`                | Everything about one facility                                    |
| `GET`  | `/api/vaults/:vaultId/nav`            | What one unit of this facility is actually worth                 |
| `GET`  | `/api/vaults/:vaultId/gate`           | Is this facility gated, and by whom                              |
| `GET`  | `/api/vaults/:vaultId/collateral`     | What pledged shares are worth to a lender                        |
| `GET`  | `/api/vaults/:vaultId/broker-history` | What this manager actually did                                   |
| `GET`  | `/api/mpt/:mptId/nav`                 | Same valuation, keyed by the share token                         |
| `GET`  | `/api/mpt/:mptId/resolve`             | Can a stranger get from a token to a valuation                   |
| `GET`  | `/api/indexer-race`                   | Proof that the first impairment is invisible to metadata diffing |
| `GET`  | `/api/demo/state`                     | Which facilities are baked and which the demo targets            |

***

## Six Response Contracts

These rules apply to every route. They are what makes the responses safe to consume at scale.

### 1. Money Is a Decimal String in Drops

One XRP is 10⁶ drops. Every monetary quantity in every response is a plain decimal string of drops — never a JSON number.

XRPL `NUMBER` fields carry up to 19 significant digits and may arrive in scientific notation. Parsing them into a JavaScript `number` destroys precision silently; `BigInt("1e17")` throws outright. Use `decimal.js` at precision 40.

<CodeGroup>
  ```js Wrong theme={null}
  // Both of these silently destroy precision
  const nav = Number(body.navCorrect)
  const assets = parseFloat(body.assetsTotal)
  ```

  ```js Right theme={null}
  import Decimal from 'decimal.js'
  Decimal.set({ precision: 40 })

  const assets = new Decimal(body.assetsTotal)  // "51000000" drops
  const nav    = new Decimal(body.navCorrect)   // "0.803922"
  ```
</CodeGroup>

A real captured value that breaks a float: `"periodicPayment": "3333346.017258355048"`.

<Tip>
  For chart pixel positions, `Number(x)` after dividing to a human scale (e.g. dividing drops by 1 000 000 to get XRP) is acceptable. For anything displayed as a monetary figure, it is not.
</Tip>

### 2. Absent Means Zero

rippled omits any field whose value equals the type default. A vault carrying no unrealised loss has **no `LossUnrealized` key at all** in the raw ledger object — not `"0"`. Orma normalises this in one place (`num()` in `src/num.mjs`) and guarantees that every numeric field in every response is always emitted, defaulting to `"0"` when the ledger omits it.

<Note>
  Two deliberate exceptions use `null` to mean "not known" rather than "zero":

  * On `broker-history`, `debtBefore`, `debtAfter`, `coverBefore`, and `coverAfter` are `null` when `brokerStateKnown` is `false`. An impairment does not touch the `LoanBroker` object, so the transaction genuinely does not report the book. Emitting `"0"` would read as an empty book — a different and worse claim.
  * On the `pledge` block of `/api/mpt/:mptId/nav` and `/api/mpt/:mptId/resolve`, `valueHeld`, `valueReported`, and `overstatement` are `null` when the resolved document carries no `unitValue`. An unvaluable pledge is not a pledge worth zero.
</Note>

### 3. Every 200 Carries a Ledger Stamp

`serverTime` and `ledgerIndex` are prepended to every successful response body. `serverTime` is the **ledger close time** converted from the Ripple epoch (offset `946684800`) — not the server's wall clock. Two fields read in the same response come from the same ledger.

`GET /api/health` builds these two fields itself rather than going through `stamp()`, because it reports on the reader's state before the reader has necessarily connected.

### 4. Default List Order Is Worst-First by Grade

`GET /api/vaults` sorts by `gradeNumeric` ascending. It does **not** sort by `navDivergenceBps`.

`navDivergenceBps` only becomes non-zero once a loss has been declared. A manager sitting on an overdue book who has declared nothing reports a divergence of zero — a divergence sort would put the most dangerous facility at the bottom of the list. In the live capture, `Kestrel Bridge Financing II` sorts first at `gradeNumeric: 26` with `navDivergenceBps: 0`, above `Meridian Trade Finance I` at `gradeNumeric: 89` with `navDivergenceBps: 1961`. That inversion is the thesis, not a sorting bug: Meridian declared its loss and holds cash against every remaining claim; Kestrel has two distressed loans and reports par.

### 5. Errors

Every error response uses this envelope:

```json theme={null}
{
  "error": {
    "code": "VAULT_NOT_FOUND",
    "message": "No vault with that id",
    "retryable": false
  }
}
```

| Code                | HTTP Status | Retryable | Raised When                                                          |
| ------------------- | ----------- | --------- | -------------------------------------------------------------------- |
| `VAULT_NOT_FOUND`   | 404         | false     | The id is well-formed but no snapshot exists for it                  |
| `NO_SHARE_MPT`      | 404         | false     | The vault has no share issuance, so there is nothing to value        |
| `NO_BROKER`         | 404         | false     | The vault has no `LoanBroker`, so there is no conduct to reconstruct |
| `SHARE_NOT_TRACKED` | 404         | false     | No facility on this service issues that MPT                          |
| `NOT_FOUND`         | 404         | false     | No route matched, or the demo capture file is missing                |
| `FORBIDDEN`         | 403         | false     | An operator `POST` from an address other than loopback               |
| `INTERNAL`          | 500         | true      | Anything unhandled                                                   |

A vault id must match `[A-Fa-f0-9]{64}` and an MPT issuance id `[A-Fa-f0-9]{48}`. Lowercase hex is accepted and upper-cased before lookup.

### 6. CORS and Public Cache Headers

Every response carries `Access-Control-Allow-Origin: *`, `Access-Control-Allow-Headers: *`, and `Access-Control-Allow-Methods: GET,POST,OPTIONS` unconditionally. `OPTIONS` returns `204`.

The default cache policy is `Cache-Control: no-store`. Three routes opt into `Cache-Control: public, max-age=4`:

* `GET /api/vaults/:vaultId/nav`
* `GET /api/mpt/:mptId/nav`
* `GET /api/mpt/:mptId/resolve`

These routes exist to be called by parties with no relationship to Orma — that is the point of writing a URL into a token. They serve read-only public ledger state, and `max-age=4` matches the 4 000 ms poll interval in `src/poll.mjs`, so a cache can never serve a figure older than one reader tick.

<Warning>
  The metadata that carries the NAV URL is written into the `MPTokenIssuance` at creation and can never be rewritten. The contract at the end of those three routes is therefore effectively permanent: fields may be added, but nothing may ever be renamed, retyped, or removed.
</Warning>
