<!-- fivemad.com, Markdown source of https://fivemad.com/redm/docs/ai/api -->
> Source: https://fivemad.com/redm/docs/ai/api
> Section: AI and API
> Tags: API, JSON, Lua, joaat, RDR2, game text, datasets, PerformHttpRequest
> Last updated: 2026-10-01
> Site map for assistants: https://fivemad.com/redm/llms.txt

# RedM API

Look up RDR2 joaat hashes, search RDR2's game text, search the RedM docs and fetch the RedM datasets over plain HTTP, with examples in Lua, JavaScript, Python and curl.

The data behind the RedM tools is reachable over HTTP. There is no key, no account and no quota to apply for. **Every endpoint is a plain `GET` with query parameters**, so anything that can fetch a URL can use it: a RedM resource, a build script, your editor, or an assistant you pointed at [/redm/llms.txt](/redm/llms.txt).

If what you want is for ChatGPT, Claude or Cursor to read these docs, that is the other page: [using fivemad RedM with an AI assistant](/redm/docs/ai).

| What | Where |
| --- | --- |
| Every RedM dataset, with its row shape and size | [`/redm/data/index.json`](/redm/data/index.json) |
| joaat hash of an RDR2 name, both directions | `/api/redm/hash?q=` |
| RDR2's English game text | `/api/redm/text?q=` |
| Search RedM tools, docs and datasets | `/api/redm/search?q=` |

The FiveM (GTA V) endpoints are documented separately on the [FiveM API page](/docs/ai/api). The two sets share their rate limits, see [Rate limits](#rate-limits).

## Calling it

One call, four ways. After this section the examples use the shortest form that makes the point, because the request is always just a URL.

### Lua, inside a resource

`PerformHttpRequest` is a server side native and works the same on RedM as on FiveM. The callback gets `(status, body, headers)`, where `status` is the HTTP status code (or `0` when the request never got out) and `body` is the raw response text, so it needs `json.decode`.

```lua title="server.lua"
local API = "https://fivemad.com/api/redm"

--- Look up an RDR2 hash or name. cb(result) gets one entry of `results`, or nil.
local function lookupHash(query, cb)
    local url = ("%s/hash?q=%s"):format(API, query)
    PerformHttpRequest(url, function(status, body, headers)
        if status ~= 200 then
            print(("[fivemad] lookup failed, HTTP %s"):format(status))
            return cb(nil)
        end
        local data = json.decode(body)
        cb(data and data.results and data.results[1] or nil)
    end, "GET")
end

RegisterCommand("rdrhash", function(source, args)
    lookupHash(args[1] or "", function(r)
        if not r then return end
        print(("[fivemad] %s -> %d match(es)"):format(r.query, r.matchCount))
        for _, m in ipairs(r.matches) do
            print(("  %s  %s"):format(m.name, m.kindLabel))
        end
    end)
end, true)
```

> [!WARNING]
> Call this from a command, an admin tool or a one off script, and cache what you get. Do not put it in a tick, a loop over every entity or anything that runs per player: it is a documentation API, not a game service, and the rate limits below are real. If you need a lookup on the hot path, fetch the dataset once at resource start and keep the table in memory.

### JavaScript, in the browser or in Node

The `/api/redm/*` endpoints send `access-control-allow-origin: *`, so this works from a page as well as from Node.

```js title="lookup.mjs"
const API = "https://fivemad.com/api/redm";
const res = await fetch(`${API}/hash?q=0xC8DA3400`);
const data = await res.json();

for (const m of data.results[0].matches) console.log(m.name, m.kind);
```

```txt
a_c_horse_arabian_white ped
```

### Python

Nothing to install; the standard library is enough.

```py title="lookup.py"
import json, urllib.request

API = "https://fivemad.com/api/redm"

with urllib.request.urlopen(f"{API}/hash?q=0xC8DA3400") as r:
    data = json.load(r)

for m in data["results"][0]["matches"]:
    print(m["name"], m["kind"])
```

```txt
a_c_horse_arabian_white ped
```

### curl

```bash
curl "https://fivemad.com/api/redm/hash?q=0xC8DA3400"
```

All four get the same answer:

```json
{
  "game": "rdr3",
  "build": "1.0.1491.50",
  "query": "0xC8DA3400",
  "searched": {
    "names": 28747,
    "sets": 17,
    "byKind": {
      "ped": 1982, "weapon": 137, "ammo": 69, "vehicle": 115, "blip": 628, "blipStyle": 620,
      "control": 373, "promptHold": 8, "scenario": 2666, "timecycle": 1286, "weather": 22,
      "zone": 87, "interior": 278, "imap": 5124, "script": 1889, "item": 13085, "document": 378
    }
  },
  "note": "joaat is a 32 bit hash, so a value can belong to more than one name. Every match is listed; none is picked for you. A label match is the game's own English text stored under that hash.",
  "limit": "60 requests per minute per IP",
  "count": 1,
  "results": [
    {
      "query": "0xC8DA3400",
      "readAs": "hash",
      "hash": { "unsigned": 3369743360, "signed": -925223936, "hex": "0xC8DA3400" },
      "matchCount": 1,
      "matches": [
        {
          "name": "a_c_horse_arabian_white",
          "kind": "ped",
          "kindLabel": "Ped model",
          "page": "https://fivemad.com/redm/assets?t=peds&q=a_c_horse_arabian_white"
        }
      ]
    }
  ]
}
```

## The datasets

The RDR2 data behind the RedM tools is a set of static files under `/data/rdr3`. It is the same file the tool itself fetches, so nothing is held back. Start with the index, which is generated from the real files:

```bash
curl https://fivemad.com/redm/data/index.json
```

```json
{
  "site": "https://fivemad.com/redm",
  "game": "rdr3",
  "generated": "2026-10-01",
  "docs": "https://fivemad.com/redm/docs/ai/api",
  "llms": "https://fivemad.com/redm/llms.txt",
  "count": 19,
  "totalBytes": 8435812,
  "datasets": [
    {
      "id": "rdr3-peds",
      "title": "Ped models",
      "url": "https://fivemad.com/data/rdr3/peds.json",
      "about": "Every ped model of RDR2 (people, horses, animals, fish) with its outfit count and a category read from the name.",
      "shape": "columns + rows: [model, hash, hash_signed, category, outfits, label, name_source, town_sets]",
      "rows": 1983,
      "bytes": 153371,
      "size": "150 KB",
      "page": "https://fivemad.com/redm/assets?t=peds",
      "pageName": "RedM Assets"
    }
  ]
}
```

Each entry gives the URL, what the file holds, how a row is laid out, how many rows there are and how big the download is. Most files carry a `columns` array and `rows` of short arrays in that order, so read a row by column name, not by position. Every hashed file also stores the hash twice: `hash` unsigned, and `hash_signed`, the same bits as a signed int, which is what `GetHashKey` returns.

The largest is the game text, 259,297 labels in a 3.8 MB brotli file. Do not download it to look up one line: use [`/api/redm/text`](#game-text) instead.

Loading a dataset once at resource start is the right pattern for a server:

```lua title="server.lua"
local peds = nil

CreateThread(function()
    PerformHttpRequest("https://fivemad.com/data/rdr3/peds.json", function(status, body)
        if status ~= 200 then return end
        local data = json.decode(body)
        peds = {}
        for _, row in ipairs(data.rows) do
            local r = {}
            for i, col in ipairs(data.columns) do r[col] = row[i] end
            peds[r.model] = r -- 1,983 rows, about 150 KB
        end
    end, "GET")
end)
```

> [!NOTE]
> These files change when we rebuild them from the game, not per request. On a server you restart often, ship the file with your resource instead and update it when you feel like it.

## Look up a joaat hash

`/api/redm/hash` takes a hash or a name. A number is looked up in every RDR2 name set we read from the game's files, plus the game's English text by label hash; a name is hashed and looked up the same way, so the answer also says whether RDR2 really has that name. Today that is 28,747 names in 17 sets, and `searched.byKind` in every answer lists them with their counts.

- `q` accepts an unsigned value, a signed int32, `0x` hex, or a name. Up to 25 values in one call, separated by commas or new lines.
- `kind` narrows the matches, for example `kind=ped,vehicle`. The kinds are `ped`, `weapon`, `ammo`, `vehicle`, `blip`, `blipStyle`, `control`, `promptHold`, `scenario`, `timecycle`, `weather`, `zone`, `interior`, `imap`, `script`, `item`, `document` and `label` (a game text label).
- A name comes back with its unsigned, signed and hex forms and the Lua backtick literal.

> [!IMPORTANT]
> joaat is a 32 bit hash, so a value can belong to more than one name. Every match is returned and none is picked for you. When `matchCount` is above 1, decide from context.

A real one: `INPUT_JUMP` is both a control and the key of a game text label, so both come back:

```bash
curl "https://fivemad.com/api/redm/hash?q=INPUT_JUMP"
```

```json
{
  "results": [
    {
      "query": "INPUT_JUMP",
      "readAs": "name",
      "name": "INPUT_JUMP",
      "lua": "`input_jump`",
      "hash": { "unsigned": 3654345152, "signed": -640622144, "hex": "0xD9D0E1C0" },
      "matchCount": 2,
      "matches": [
        {
          "name": "INPUT_JUMP",
          "kind": "control",
          "kindLabel": "Control (input)",
          "page": "https://fivemad.com/redm/assets?t=controls&q=INPUT_JUMP"
        },
        {
          "name": "INPUT_JUMP",
          "kind": "label",
          "kindLabel": "Game text label: \"Jump\"",
          "page": "https://fivemad.com/redm/tools/text-search?q=0xD9D0E1C0",
          "text": "Jump"
        }
      ]
    }
  ]
}
```

With `kind=control` only the first match is left. Several controls at once, in Python:

```py title="controls.py"
import json, urllib.parse, urllib.request

API = "https://fivemad.com/api/redm"
names = ["INPUT_JUMP", "INPUT_ENTER", "INPUT_CONTEXT_Y"]
url = f"{API}/hash?" + urllib.parse.urlencode({"q": ",".join(names), "kind": "control"})

with urllib.request.urlopen(url) as r:
    data = json.load(r)

for res in data["results"]:
    print(res["name"], res["hash"]["hex"], res["hash"]["signed"], res["matchCount"])
```

```txt
INPUT_JUMP 0xD9D0E1C0 -640622144 1
INPUT_ENTER 0xCEFD9220 -822242784 1
INPUT_CONTEXT_Y 0xD51B784F -719620017 1
```

`matchCount: 0` means we have no record of that name or value, not that it does not exist: the game stores most of its names only as hashes, and a resource can add its own. The page for the same lookup is the [Hash Calculator](/redm/docs/tools/hash-calculator), under Reverse lookup.

## Game text

`/api/redm/text` searches RDR2's American English text: 259,297 labels, 183,520 of them spoken lines. The text stays on our server, so a call answers in a few hundred bytes instead of a 3.8 MB download.

| Parameter | What it does | Default |
| --- | --- | --- |
| `q` | At least two characters. Words (every word has to be in the line or its key name), a label hash (`0x0000F96B`, `63851`, 8 hex digits) or a label key. | required |
| `kind` | `all`, `dialogue` (spoken lines) or `ui` (everything else). | `all` |
| `limit` | Rows to return, 1 to 200. | 50 |

A query that reads as a hash or hashes to a label key is answered in `exact` first. The word search returns rows in label hash order, and `total` is the full count, not just what fits in `limit`.

```bash
curl "https://fivemad.com/api/redm/text?q=saloon&limit=3"
```

```json
{
  "query": "saloon",
  "kind": "all",
  "game": "rdr3",
  "build": "1.0.1491.50",
  "searched": { "labels": 259297, "dialogue": 183520, "withKey": 27294 },
  "exact": [],
  "total": 294,
  "count": 3,
  "ms": 9,
  "results": [
    {
      "hash": { "unsigned": 4324998, "signed": 4324998, "hex": "0x0041FE86" },
      "key": "",
      "text": "Bill, Micah and Sean are meeting the Grays over at the saloon about a job.",
      "raw": "Bill, Micah and Sean are meeting the Grays over at the saloon about a job.",
      "dialogue": false
    },
    {
      "hash": { "unsigned": 35507793, "signed": 35507793, "hex": "0x021DCE51" },
      "key": "BRT1_F_HARMOUTS",
      "text": "The saloon staff were harmed.",
      "raw": "The saloon staff were harmed.",
      "dialogue": false
    },
    {
      "hash": { "unsigned": 44939184, "signed": 44939184, "hex": "0x02ADB7B0" },
      "key": "",
      "text": "WANTED for the ATTEMPTED MURDER of well-liked saloon proprietor, Carl Evans, and the abduction of Mrs. Evans, who has since escaped CORBETT'S custody.",
      "raw": "WANTED for the ATTEMPTED MURDER of well-liked saloon proprietor, Carl Evans, and the abduction of Mrs. Evans, who has since escaped CORBETT'S custody.",
      "dialogue": false
    }
  ]
}
```

`text` is the line made readable (`~n~` becomes a line break and the formatting codes go); `raw` is exactly what the game stores, markup included. `key` is the label's name when one is known, which is true for 27,294 labels; the rest are known only by hash.

By hash:

```bash
curl "https://fivemad.com/api/redm/text?q=0x0000F96B"
```

```json
{
  "exact": [
    {
      "hash": { "unsigned": 63851, "signed": 63851, "hex": "0x0000F96B" },
      "key": "",
      "text": "Jumping Jeremiah!",
      "raw": "~z~Jumping Jeremiah!",
      "dialogue": true
    }
  ],
  "total": 0,
  "count": 0,
  "results": []
}
```

The `~z~` in front is how the game marks a spoken line. In JavaScript, spoken lines only:

```js title="text.mjs"
const API = "https://fivemad.com/api/redm";
const res = await fetch(`${API}/text?q=${encodeURIComponent("jumping jeremiah")}&kind=dialogue`);
const data = await res.json();

console.log(`${data.total} line(s) of ${data.searched.labels}`);
for (const r of data.results) console.log(r.hash.hex, r.text);
```

```txt
2 line(s) of 259297
0x0000F96B Jumping Jeremiah!
0xDC7DB265 Jumping Jeremiah.
```

The page for the same search, with the Lua to replace a line in RedM, is the [Game Text Search](/redm/docs/tools/text-search).

## Search

One short JSON answer over the RedM tools, the RedM docs and the RedM dataset index. It is the cheapest way for a script or an assistant to find out what exists without downloading anything.

```bash
curl "https://fivemad.com/api/redm/search?q=prompt&limit=3"
```

```json
{
  "game": "rdr3",
  "query": "prompt",
  "type": "all",
  "total": 10,
  "returned": 3,
  "limit": "60 requests per minute per IP",
  "hint": "Add .md to any /redm/docs URL for the plain Markdown source.",
  "results": [
    {
      "type": "doc",
      "title": "Prompt Builder",
      "url": "https://fivemad.com/redm/docs/tools/prompt-builder",
      "markdown": "https://fivemad.com/redm/docs/tools/prompt-builder.md",
      "summary": "Build a group of RDR2 prompts (press, hold or mash) on the game's own input actions and hold timings, try them in a preview, and download the Lua.",
      "extra": {
        "section": "Tools",
        "match": "The Prompt Builder makes the \"press E to...\" boxes RDR2 shows in the bottom right of the screen. You set up a group of prompts, try each one in a preview that behaves like the real thing, and get a ready resource: a..."
      },
      "score": 117.5
    },
    {
      "type": "tool",
      "title": "Prompt Builder",
      "url": "https://fivemad.com/redm/tools/prompt-builder",
      "summary": "Build a group of RDR2 prompts (press, hold or mash) on the game's own controls and hold timings, preview it, and copy the Lua.",
      "extra": { "group": "Generators" },
      "score": 105
    },
    {
      "type": "doc",
      "title": "Porting a FiveM resource to RedM",
      "url": "https://fivemad.com/redm/docs/porting",
      "markdown": "https://fivemad.com/redm/docs/porting.md",
      "summary": "What changes when a FiveM resource moves to RedM, in the order you hit it: the game key, natives by hash, controls, peds and outfits, prompts, blips and game data.",
      "extra": {
        "section": "Porting from FiveM",
        "match": "hing, tell InvokeNative what to read back, the same way the Prompt Builder output does it: local N = Citizen.InvokeNative local p = N(0x04F97DE45A519419, Citizen.ResultAsInteger()) -- UI_PROMPT_REGISTER_BEGIN Go through..."
      },
      "score": 37.5
    }
  ]
}
```

Parameters: `q` (required), `type` (`all`, `tools`, `docs`, `datasets`), `limit` (1 to 50, default 10). Every docs hit carries its `.md` URL, so the next step is one more fetch. The `score` only explains the order. `type=datasets` answers with the file URL, its shape and the page that uses it:

```bash
curl "https://fivemad.com/api/redm/search?q=doors&type=datasets"
```

```json
{
  "results": [
    {
      "type": "dataset",
      "title": "Doors",
      "url": "https://fivemad.com/data/rdr3/doors.json",
      "summary": "Every door RDR2 registers with its door system: door hash, model, position, heading and the interior it is in.",
      "extra": {
        "id": "rdr3-doors",
        "shape": "columns + rows: [hashHex, hash, signed, model, modelHash, x, y, z, heading, interior, interiorName, script, copies]",
        "page": "https://fivemad.com/redm/tools/door-finder"
      },
      "score": 90
    }
  ]
}
```

## Rate limits

Per IP, in a fixed one minute window. The RedM endpoints draw from the same budgets as the FiveM ones:

| Endpoint | Budget | Limit |
| --- | --- | --- |
| `/api/redm/hash` | `hash`, shared with `/api/hash` | 60 per minute |
| `/api/redm/text` | `search`, shared with `/api/search` and `/api/redm/search` | 60 per minute |
| `/api/redm/search` | `search`, as above | 60 per minute |
| Every public API call, FiveM and RedM together | `all` | 240 per minute |

So 40 text searches and 20 RedM searches in one minute use up the `search` budget together. Over the limit you get `429` with a `retry-after` header in seconds and this body (a real one):

```json
{
  "error": "rate_limited",
  "message": "Too many requests. This endpoint allows 60 requests per minute per IP (240 across the whole API). Try again in 60s.",
  "docs": "https://fivemad.com/redm/docs/ai/api"
}
```

The static files (`/redm/data/index.json`, `/data/rdr3/*`, the `.md` pages, `/redm/llms.txt`) are not rate limited. Please cache them rather than refetching.

## CORS, caching and errors

- The `/api/redm/*` endpoints answer with `access-control-allow-origin: *` and answer a preflight `OPTIONS` with `204`, so a browser based agent can call them from any page. `POST` with a JSON body works the same as the query string.
- A `200` answer is cached for 60 seconds (`cache-control: public, max-age=60`); an error is `no-store`.
- The static files are served by the front end web server, not by the app, so whether they carry a CORS header depends on the deployment. From `PerformHttpRequest`, curl or an assistant's own fetcher this makes no difference.
- Errors are JSON with an `error` code, a `message` written for a human, and a `docs` link. For a `/api/redm/*` call the link points at this page; the FiveM endpoints point at the [FiveM API page](/docs/ai/api).

| Status | `error` | When |
| --- | --- | --- |
| 400 | `invalid` | `q` is missing (or shorter than two characters on `/text`) |
| 400 | `bad_kind` | an unknown `kind` on `/hash` or `/text` |
| 400 | `bad_type` | an unknown `type` on `/search` |
| 429 | `rate_limited` | over a limit above |
| 503 | `unavailable` | the name index or the game text could not be loaded on the server |

```bash
curl "https://fivemad.com/api/redm/hash?q=1&kind=nope"
```

```json
{
  "error": "bad_kind",
  "message": "Unknown kind \"nope\". Known kinds: ped, weapon, ammo, vehicle, blip, blipStyle, control, promptHold, scenario, timecycle, weather, zone, interior, imap, script, item, document, label.",
  "docs": "https://fivemad.com/redm/docs/ai/api"
}
```

A value that is neither a hash nor a usable name is not an error for the whole call: that one entry of `results` carries `"error": "invalid"` and the rest are answered.

## A worked example

"Spawn a white Arabian next to the player, and make sure the model name is real."

```bash
# 1. is the name real, and what is its hash?
curl "https://fivemad.com/api/redm/hash?q=a_c_horse_arabian_white"
```

```json
{
  "results": [
    {
      "query": "a_c_horse_arabian_white",
      "readAs": "name",
      "name": "a_c_horse_arabian_white",
      "lua": "`a_c_horse_arabian_white`",
      "hash": { "unsigned": 3369743360, "signed": -925223936, "hex": "0xC8DA3400" },
      "matchCount": 1,
      "matches": [
        {
          "name": "a_c_horse_arabian_white",
          "kind": "ped",
          "kindLabel": "Ped model",
          "page": "https://fivemad.com/redm/assets?t=peds&q=a_c_horse_arabian_white"
        }
      ]
    }
  ]
}
```

It is a ped model (on RDR2 a horse is a ped), so the spawn is a `CreatePed` plus the outfit call every RDR2 ped needs:

```lua title="client.lua"
local model = `a_c_horse_arabian_white` -- 0xC8DA3400
RequestModel(model)
while not HasModelLoaded(model) do Wait(0) end
local c = GetEntityCoords(PlayerPedId())
local ped = CreatePed(model, c.x + 2.0, c.y, c.z, 0.0, true, false, false, false)
-- _SET_RANDOM_OUTFIT_VARIATION: a RedM ped with no outfit is invisible
Citizen.InvokeNative(0x283978A15512B2FE, ped, true)
SetModelAsNoLongerNeeded(model)
```

Why the outfit line is there is explained in [Porting a FiveM resource](/redm/docs/porting). The same steps are what an assistant does when you ask it the question, which is covered on the [assistant page](/redm/docs/ai).
