Hungrit Docs

http#

Calls a service outside the world: a leaderboard site, a chat webhook, your own database, a payment API.

Use it to

  • post to a chat webhook when something happens
  • read or update a leaderboard on your own site
  • call any HTTPS service from inside the world
Luau
local http = require("http")

Not for saving a player's progress

players.store already saves per player, per world, with no network in the way and no limits like the ones below. Reach for http when you need to talk to somebody else's service.

send#

Luau
http:send(request, onAnswer) -> true | nil, reason
Luau
local http = require("http")local json = require("json") http:send({    url = "https://api.example.com/score",    method = "POST",    headers = { ["content-type"] = "application/json" },    body = json.encode({ name = "ana", points = 42 }),}, function(res)    if res.ok then        print("saved")    else        print("failed: " .. (res.error or tostring(res.status)))    endend)

One verb. The method is a field, not a different function.

field
urlrequired. https:// only, port 443
methodGET (default), POST, PUT, PATCH, DELETE
headersnames are lower-cased; up to 16
bodyup to 64 KB. A GET cannot carry one

The callback is optional. Leave it out for a call whose answer you do not care about, like a notification.

The answer arrives later#

This is the one thing to hold on to. The internet takes time, and the world cannot stop and wait for it. Every script, the physics and every player standing in the region would freeze along with it. So send returns straight away and your function is called when the answer comes back.

Three consequences:

It is a new event. The callback gets a fresh instruction budget, and the count of things it may change in the world starts again at 8. It is not a continuation of the handler that called send.

The order is not promised. Two requests come back in whatever order the network settles them.

Luau
-- Wrong: `total` may be read before either answer arrived.local total = 0http:send({ url = a }, function(res) total = total + 1 end)http:send({ url = b }, function(res) total = total + 1 end)print(total)        -- 0 -- Right: count inside, and act when the last one lands.local left = 2local function done()    left = left - 1    if left == 0 then print("both are in") endendhttp:send({ url = a }, done)http:send({ url = b }, done)

The world moved on. The callback reads the world as it is now, and a second is a long time in a region. The player who touched the button may have walked away; check before acting on them.

What comes back#

Luau
http:send({ url = "https://api.example.com/who" }, function(res)    print(res.ok)                          -- true when it answered 2xx    print(res.status)                      -- 200, 404, 500… (0 = no answer)    print(res.body)                        -- text    print(res.headers["content-type"])     -- names are lower-case    print(res.error)                       -- nil when it answered at allend)

A 404 is not an error. Neither is a 500. Those are answers, and res.error stays nil for both, so check res.ok or res.status for what the far end said. res.error is for when there was no answer: a timeout, a refused connection, a body too big to accept.

Refusals#

send answers nil and a reason instead of raising, like everything else in this API. A refusal here means the callback will not run. Nothing was sent.

reasonwhat to do
permissionthe world has not granted net to this script's owner
http busytoo many requests waiting; back off and retry
http coolingthat address has been failing, and is left alone for 30 s
http quotathe world has spent its traffic for the day
too many requests in one eventan event may change at most 8 things, and a request is one
http: …the request itself: the address, a header or a size. The text says which

Addresses that are refused#

Luau
http:send({ url = "http://api.example.com/" })      -- nil, only httpshttp:send({ url = "https://api.example.com:8080/" })-- nil, only port 443http:send({ url = "https://127.0.0.1/" })           -- nil, not reachablehttp:send({ url = "https://user:[email protected]/" }) -- nil, no userinfo

Only https://, only port 443, and only addresses out on the public internet. An address that points inside the machine hosting the world is refused, and so is a name that resolves to one, because the check happens against the address the connection is actually about to use.

Plain http:// is not allowed on purpose. In plain text, anyone between the world and the far end can not only read the answer but rewrite it, and the answer is what your script then acts on.

Redirects are not followed#

A 3xx comes back as an ordinary answer. Read it and decide:

Luau
http:send({ url = first }, function(res)    if res.status == 301 or res.status == 302 then        local where = res.headers["location"]        if where then            http:send({ url = where }, function(res2) end)        end    endend)

Headers the engine owns#

host, content-length, connection, transfer-encoding, upgrade, expect, cookie and anything starting proxy- or x-hungrit- are set by the engine and cannot be changed. There are no cookies at all, so every request stands alone.

Every request carries x-hungrit-world and x-hungrit-object, so a service you call can tell your world apart from every other one.

An API key in a script is a public API key

Scripts are open. Anybody who can read yours can read the token in it. Until there is a place to keep a secret, treat any key you put in a script as published, and prefer a service of your own in front of the one holding the real key.

Limits#

See limits for the full table. In short: 10 requests a second per world, banking up to 60 for a burst; 64 KB out and 256 KB back; 10 seconds before it gives up.

Updated 17 October 2026