GetParentResourceName, callback URLs and what happens to your fetch
GetParentResourceName() is a function FiveM injects into NUI pages; it returns the name of the resource that owns the frame. A POST to https://<that name>/<callback> never leaves the game: FiveM intercepts it, parses the body as JSON and hands it to the handler registered under that callback name. If the body is not valid JSON, the handler is never called and the fetch hangs. RegisterNUICallback is the older event-based Lua helper, RegisterNuiCallback the direct one; both behave the same from the page. RegisterRawNuiCallback gives you the raw request (method, path, headers, body) and lets you set the status, headers and body of the response.
Every NUI tutorial shows the same line: fetch(`https://${GetParentResourceName()}/something`). Few explain what that URL is, why it is not a real website, what FiveM does with the body, and why a request can hang forever without a single error. This article follows one request from the page to your Lua function and back, and covers the lesser-known raw callback API for cases the normal one cannot handle.

#What GetParentResourceName is
It is not a browser API and not part of any library. FiveM defines GetParentResourceName as a global function inside NUI frames, and it returns the name of the resource whose ui_page created the frame. The name is the folder name the server owner used, which is exactly why you read it at runtime: if someone renames my-phone to phone, a hard-coded URL breaks and GetParentResourceName() keeps working.
Because it only exists in game, it is undefined in a normal browser tab during development. Guard it once in a helper rather than scattering typeof checks through your code:
declare global {
interface Window {
GetParentResourceName?: () => string;
}
}
export const inGame = typeof window.GetParentResourceName === 'function';
export const resourceName = inGame ? window.GetParentResourceName!() : 'dev-resource';#Following one request
- The page calls
fetch(`https://${GetParentResourceName()}/buyItem`, { method: 'POST', body: JSON.stringify({ item: 'water' }) }). - FiveM’s browser layer recognises the host as a resource name and intercepts the request. Nothing goes to the network, so no DNS, no TLS and no CORS apply.
- The part after the host (
buyItem) selects the callback registered under that name in that resource. - The body is parsed as JSON and converted into a Lua table (or a JS object in a JavaScript client script).
- Your handler runs and calls
cb(result). The result is encoded back to JSON and returned with status 200 and a JSON content type. - The page’s
fetchresolves;await res.json()gives youresult.
The hand-off is asynchronous in both directions. Your handler can wait for a server callback before calling cb, and the page simply waits on the promise. The practical helper that wraps this pattern is in NUI callbacks with fetch.
#Body rules that cause silent failures
Looking at how FiveM handles the request explains two of the most confusing bugs. The body is parsed as JSON before your handler is called. If parsing fails, the handler is simply not invoked, so cb is never called and the request stays pending with no error on either side.
| Page sends | Handler receives | Result |
|---|---|---|
JSON.stringify({ a: 1 }) | { a = 1 } | Works |
'{}' | An empty table | Works; the safest “no data” body |
| No body at all | nil | Works, but guard against indexing nil |
'hello' (plain text) | Nothing, handler never runs | Fetch hangs forever |
{ a: 1 } (object, not stringified) | Nothing; the body becomes [object Object] | Fetch hangs forever |
FormData | Nothing, not JSON | Fetch hangs forever |
The response side has its own rule: cb must be called exactly once, on every path, and its argument must be encodable. Tables, strings, numbers and booleans are fine. Functions, entity handles you expect to be objects, and tables with mixed or sparse keys produce surprising JSON; see SendNUIMessage patterns for the encoder’s quirks, which apply equally here.
#RegisterNUICallback, RegisterNuiCallback and RegisterNuiCallbackType
Three names appear in scripts and all of them still work. They differ in how the Lua runtime wires them up, not in what the page sees.
| Function | How it works | When to use |
|---|---|---|
RegisterNuiCallback(name, fn) | Registers a direct function reference with the NUI layer | New code in Lua, JS or C# |
RegisterNUICallback(name, fn) | Lua helper: calls RegisterNuiCallbackType(name) and listens to the __cfx_nui:<name> event | Existing Lua code; behaves the same |
RegisterNuiCallbackType(name) | Declares the type so requests are routed to the __cfx_nui:<name> event | Only when handling that event manually |
RegisterRawNuiCallback(name, fn) | Direct handler that receives the raw HTTP-like request | Non-JSON bodies, custom status codes, sub-paths |
The event-based variant exists for compatibility: some older scripts listen to __cfx_nui: events directly. For anything you write today, RegisterNuiCallback is the straightforward choice, and in Lua both helpers catch errors thrown inside your handler and print them to the client console as “error during NUI callback”.
#Raw NUI callbacks
RegisterRawNuiCallback skips the JSON conversion. Your handler receives a request table and answers with a response table. From FiveM’s source, the request carries method (POST when there is a body, otherwise GET), path (whatever follows the callback name, plus the query string), headers (a key/value table), rawHeaders (an array of { key, value } pairs), body (the raw string) and, when known, resource (the resource whose page made the call).
RegisterRawNuiCallback('api', function(req, cb)
if req.method ~= 'POST' then
cb({ status = 405, body = 'method not allowed' })
return
end
local ok, payload = pcall(json.decode, req.body)
if not ok or type(payload) ~= 'table' then
cb({ status = 400, headers = { ['Content-Type'] = 'text/plain' }, body = 'bad json' })
return
end
if req.path == '/notes/save' then
SetResourceKvp('notes', payload.text or '')
cb({ status = 200, headers = { ['Content-Type'] = 'application/json' }, body = json.encode({ ok = true }) })
return
end
cb({ status = 404, body = 'unknown route' })
end)const res = await fetch(`https://${GetParentResourceName()}/api/notes/save`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'Meet at the pier' }),
});
if (!res.ok) console.warn('save failed', res.status, await res.text());Raw callbacks are worth it when the page sends something that is not JSON (plain text, a large base64 string you want to pass through untouched), when you want real HTTP status codes so res.ok means something, or when one handler should serve a small set of routes. For ordinary buttons and forms, the normal callback is simpler.
#Never let a callback hang the UI
Even with perfect code, a Lua handler that waits on the server can take a while, and a UI that freezes on a pending promise feels broken. Give every call a timeout on the page side so the interface can recover and show an error.
export async function fetchNui<T>(name: string, data: unknown = {}, timeoutMs = 8000): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(`https://${resourceName}/${name}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
body: JSON.stringify(data ?? {}),
signal: controller.signal,
});
return (await res.json()) as T;
} finally {
clearTimeout(timer);
}
}Pair it with a disabled state on the button that triggered it, so impatient players cannot fire the same purchase five times while the first one is still in flight.
#What the URL does not protect
The fact that the URL contains your resource name is routing, not authentication. Anyone with the devtools open can call any of your callbacks with any body. Your Lua handler should check types and ranges, and anything involving money, items or permissions must be decided on the server. The client handler’s job is to pass along what the player chose, never what it costs.
Loading screens are the exception to all of this: a loadscreen page cannot call callbacks at all, because no Lua is running yet for it to talk to. It only receives events and handover data.
#Testing callbacks from the devtools console
You do not need to click through the UI to test a handler. With the game running, open the page in the devtools at localhost:13172 and call the callback directly from the console. Because the console runs inside the NUI frame, GetParentResourceName() works there exactly as it does in your code.
await fetch(`https://${GetParentResourceName()}/buyItem`, {
method: 'POST',
body: JSON.stringify({ item: 'water', amount: -5 }),
}).then((r) => r.json());This is also the fastest security review you can do: send the values a cheater would send (negative amounts, huge numbers, item names that do not exist, missing fields) and watch what your handler and the server do with them. If any of those calls changes something it should not, fix the server-side check before the release, not after the first exploit report.
Questions
What does GetParentResourceName return?
Why is GetParentResourceName not defined?
Why does my NUI fetch never resolve?
cb was not called on some path, or the body was not valid JSON, in which case the handler never runs. Always send JSON.stringify(...).What is the difference between RegisterNUICallback and RegisterNuiCallback?
__cfx_nui: events, the second registers the function directly. From the page they behave the same.When should I use RegisterRawNuiCallback?
Ready to pick a map?
Twelve themes on three base map styles, $8 each, instant download.
Relevant to what you just read