NUI and CEFintermediate11 min readupdated 19 Sept 2026

GetParentResourceName, callback URLs and what happens to your fetch

The short answer

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.

GetParentResourceName, callback URLs and what happens to your fetch — illustrated with a FiveM-compatible minimap render

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

nui.ts
declare global {
  interface Window {
    GetParentResourceName?: () => string;
  }
}

export const inGame = typeof window.GetParentResourceName === 'function';

export const resourceName = inGame ? window.GetParentResourceName!() : 'dev-resource';

#Following one request

  1. The page calls fetch(`https://${GetParentResourceName()}/buyItem`, { method: 'POST', body: JSON.stringify({ item: 'water' }) }).
  2. 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.
  3. The part after the host (buyItem) selects the callback registered under that name in that resource.
  4. The body is parsed as JSON and converted into a Lua table (or a JS object in a JavaScript client script).
  5. Your handler runs and calls cb(result). The result is encoded back to JSON and returned with status 200 and a JSON content type.
  6. The page’s fetch resolves; await res.json() gives you result.

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 sendsHandler receivesResult
JSON.stringify({ a: 1 }){ a = 1 }Works
'{}'An empty tableWorks; the safest “no data” body
No body at allnilWorks, but guard against indexing nil
'hello' (plain text)Nothing, handler never runsFetch hangs forever
{ a: 1 } (object, not stringified)Nothing; the body becomes [object Object]Fetch hangs forever
FormDataNothing, not JSONFetch 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.

FunctionHow it worksWhen to use
RegisterNuiCallback(name, fn)Registers a direct function reference with the NUI layerNew code in Lua, JS or C#
RegisterNUICallback(name, fn)Lua helper: calls RegisterNuiCallbackType(name) and listens to the __cfx_nui:<name> eventExisting Lua code; behaves the same
RegisterNuiCallbackType(name)Declares the type so requests are routed to the __cfx_nui:<name> eventOnly when handling that event manually
RegisterRawNuiCallback(name, fn)Direct handler that receives the raw HTTP-like requestNon-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).

client.lua — a raw callback with sub-paths and status codes
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)
app.js
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.

nui.ts
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.

devtools console
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.

Share this guideXRedditFacebook

Questions

What does GetParentResourceName return?
The name of the resource that owns the NUI page, exactly as the server owner named its folder.
Why is GetParentResourceName not defined?
You are running the page in a normal browser or in a context where FiveM did not inject it. Guard it and fall back to mock data during development.
Why does my NUI fetch never resolve?
Either 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?
The first is an older Lua helper built on __cfx_nui: events, the second registers the function directly. From the page they behave the same.
When should I use RegisterRawNuiCallback?
When you need the raw body, the request path or headers, or want to return your own HTTP status codes.

Ready to pick a map?

Twelve themes on three base map styles, $8 each, instant download.

Relevant to what you just read