Every docs URL answers with Markdown when you add .md. Site map: 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.
Por enquanto, esta documentação está em inglês.
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.
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.
| What | Where |
|---|---|
| Every RedM dataset, with its row shape and size | /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. The two sets share their rate limits, see 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.
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.
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);a_c_horse_arabian_white pedPython
Nothing to install; the standard library is enough.
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"])a_c_horse_arabian_white pedcurl
curl "https://fivemad.com/api/redm/hash?q=0xC8DA3400"All four get the same answer:
{
"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:
curl https://fivemad.com/redm/data/index.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 instead.
Loading a dataset once at resource start is the right pattern for a server:
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.
qaccepts an unsigned value, a signed int32,0xhex, or a name. Up to 25 values in one call, separated by commas or new lines.kindnarrows the matches, for examplekind=ped,vehicle. The kinds areped,weapon,ammo,vehicle,blip,blipStyle,control,promptHold,scenario,timecycle,weather,zone,interior,imap,script,item,documentandlabel(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:
curl "https://fivemad.com/api/redm/hash?q=INPUT_JUMP"{
"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:
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"])INPUT_JUMP 0xD9D0E1C0 -640622144 1
INPUT_ENTER 0xCEFD9220 -822242784 1
INPUT_CONTEXT_Y 0xD51B784F -719620017 1matchCount: 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, 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.
curl "https://fivemad.com/api/redm/text?q=saloon&limit=3"{
"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:
curl "https://fivemad.com/api/redm/text?q=0x0000F96B"{
"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:
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);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.
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.
curl "https://fivemad.com/api/redm/search?q=prompt&limit=3"{
"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:
curl "https://fivemad.com/api/redm/search?q=doors&type=datasets"{
"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):
{
"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 withaccess-control-allow-origin: *and answer a preflightOPTIONSwith204, so a browser based agent can call them from any page.POSTwith a JSON body works the same as the query string. - A
200answer is cached for 60 seconds (cache-control: public, max-age=60); an error isno-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
errorcode, amessagewritten for a human, and adocslink. For a/api/redm/*call the link points at this page; the FiveM endpoints point at the FiveM API page.
| 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 |
curl "https://fivemad.com/api/redm/hash?q=1&kind=nope"{
"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.”
# 1. is the name real, and what is its hash?
curl "https://fivemad.com/api/redm/hash?q=a_c_horse_arabian_white"{
"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:
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. The same steps are what an assistant does when you ask it the question, which is covered on the assistant page.