Aller au contenu
RedM API

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.

Cette documentation est en anglais pour l'instant.

Last updated

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.

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.

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);
Text
a_c_horse_arabian_white ped

Python

Nothing to install; the standard library is enough.

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"])
Text
a_c_horse_arabian_white ped

curl

Terminal
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:

Terminal
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 instead.

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

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:

Terminal
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:

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"])
Text
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, 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.

Terminal
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:

Terminal
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:

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);
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.

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.

Terminal
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:

Terminal
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.
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
Terminal
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.”

Terminal
# 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:

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. The same steps are what an assistant does when you ask it the question, which is covered on the assistant page.