Skip to content

api_structure

Francisco Dias edited this page Aug 20, 2026 · 6 revisions

1. Big picture

  • Every exported elements_... function is a thin wrapper around one HTTP request.
  • Requests are async and callback-based (GameMaker’s HTTP async event).
  • Auth is handled via named tokens ("auth_bearer", "session_secret") that you store once and which are then injected into all requests that need them.
  • Request bodies are formatted based on a Content-Type → converter mapping.
  • Responses go through an optional hook system and then into your callback.

Once you understand that pattern, every endpoint behaves the same way.


2. Calling an endpoint

All endpoints follow this shape:

function some_elements_endpoint(arg1, arg2, ..., _callback = undefined)
{
    // validate arguments
    // build URL
    // (optionally) build query params
    // define required security schemes
    return _elements_create_request(_url, _params, _method, _body, _content_type, _security, _callback, _GMFUNCTION_);
}

Example: simple GET without body

elements_get_application("my-app-id", function(_code, _data, _request) {
    if (_code == 200) {
        show_debug_message("App: " + string(_data));
    } else {
        show_debug_message("Error: " + string(_code));
    }
});
  • elements_get_application will:

    • Validate its arguments.
    • Build URL: "{rest_url}/application/my-app-id".
    • Require security: [ "auth_bearer", "session_secret" ].
    • Create an ElementsRequest and send() it.
  • It returns the HTTP request id from http_request (in case you want to track/cancel).

Example: POST with body

var _body = new ElementsCreateAppleIapReceipt(_receipt_base64, "SANDBOX");

elements_upload_apple_iap_receipt(_body, function(_code, _data, _request) {
    if (_code == 200) {
        // _data is whatever the server returns (likely JSON parsed to a struct/array)
        show_debug_message("Reward issuances: " + string(_data));
    } else {
        show_debug_message("IAP error: " + string(_code));
    }
});

Important

When a body is a struct generated by these constructors, the wrapper calls <Type>_validate() before sending to ensure types are correct.


3. Authentication model

3.1. How tokens are stored

Auth tokens live in the obj_elements_core singleton, in its auth_tokens map:

// set
_elements_request_auth_set_token("auth_bearer", some_token);
_elements_request_auth_set_token("session_secret", some_other_token);

// get
var bearer = _elements_request_auth_get_token("auth_bearer");

The singleton is created lazily:

function _elements_get_singleton(_where)
{
    static instance = instance_create_depth(0, 0, 0, obj_elements_core);
    with (instance) return self;
}

So you don’t manually create it; using any of the helper functions will.

3.2. How tokens are injected into requests

Endpoints declare which auth schemes they need:

var _security = [ "auth_bearer", "session_secret" ];

Then ElementsRequest.send() calls _apply_auth for each scheme:

_self._apply_auth(_header, _params, security[_i], where);

_apply_auth does:

case "auth_bearer":
    var _token = _elements_request_auth_get_token("auth_bearer");
    if (is_undefined(_token)) { /* log missing */ break; }
    _header[? "Authorization"] = "Bearer " + _token;
    break;

case "session_secret":
    var _token = _elements_request_auth_get_token("session_secret");
    if (is_undefined(_token)) { /* log missing */ break; }
    _header[? "Elements-SessionSecret"] = _token;
    break;

So:

  • If you don’t set a token, that scheme is simply skipped (no header added).
  • If you do set it, it will be applied automatically on all endpoints that list that scheme.

3.3. What you must do after login

From your example:

elements_create_username_password_session(_session_request, function(_code, _data, _request) {
    if (_code == 200) {
        obj_game.session_data = _data;

        var _session_secret = _data.sessionSecret;
        _elements_request_auth_set_token("auth_bearer", _session_secret);
        // (optionally) also:
        // _elements_request_auth_set_token("session_secret", _session_secret);
    }
    show_debug_message("elements_create_username_password_session :: " + string(_data));
});

Pattern:

  1. Call a login/auth endpoint.

  2. In its callback:

    • Extract token(s) from _data.
    • Store them using _elements_request_auth_set_token.
  3. After that, all other endpoints needing those schemes will automatically send the right headers.

That’s the main thing a user of this API needs to understand about auth.


4. The async HTTP flow (callbacks)

Here’s the lifecycle:

  1. You call an elements_* function.

  2. It creates an ElementsRequest and calls .send().

  3. send():

    • Builds headers and URL.
    • Calls http_request(...), gets an HTTP id.
    • Stores itself in obj_elements_core.requests[? id] = self.
  4. Later, GameMaker fires an Async HTTP event on obj_elements_core with async_load.

Inside that event (your snippet):

var _async_id = async_load[? "id"];
var _request = requests[? _async_id];

if (_request == undefined) exit;           // unknown request
var _status = async_load[? "status"];
if (_status == 1) exit;                    // still in progress

var _code = async_load[? "http_status"];
var _data = async_load[? "result"];

4.1. Response hooks (optional)

Before your callback is run, hooks are checked:

var _hook = response_hooks[? _code];

if (is_callable(_hook) && _hook(_code, _data, _request) == true) {
    ds_map_delete(requests, _async_id);
    return;
}
  • A hook is a global handler you can register per HTTP status code.
  • If it returns true, it “consumes” the response and the per-request callback is skipped.

You register a hook like:

_elements_request_response_set_hook(401, function(_code, _data, _request) {
    // global unauthorized handler, maybe redirect to login screen
    // return true to stop further handling
    return true;
});

4.2. Calling your callback

If no hook stops it, your callback is called:

var _callback = _request.get_callback();
if (is_callable(_callback)) {
    try {
        _data = json_parse(_data);
    } catch(_ex) { /* if not JSON, leave _data as string */ }
    _callback(_code, _data, _request);
}
ds_map_delete(requests, _async_id);

So:

  • _code is the HTTP status (200, 400, 500, …).

  • _data is:

    • Parsed JSON → struct/array when possible.
    • Raw string if json_parse fails.
  • _request is the ElementsRequest instance (you can inspect method, URL and even retry the request).

Your callbacks should always be ready to handle both success and error codes.


5. Content types & body conversion

5.1. How content_type is used

Every request passes a _content_type string into ElementsRequest:

  • Some endpoints fix it internally:

    static _content_type = "application/json";
    ...
    _elements_create_request(_url, _params, "POST", _body, _content_type, ...);
  • Some let you override it:

    function elements_update_product_bundle_for_application_configuration(..., _body = undefined, _content_type = "*/*", _callback = undefined)
    {
        ...
        return _elements_create_request(_url, undefined, "PUT", _body, _content_type, _security, _callback, _GMFUNCTION_);
    }

Internally, when an ElementsRequest is created:

if (!is_undefined(_body)) {
    _body = _process_body(_body, _content_type, _where);
    if (!is_string(_body)) {
        __.body_type = 1; // buffer
        __.body = buffer_base64_encode(_body, 0, -1);
    } else {
        __.body = _body;
    }
}

So:

  • _content_type controls:

    • The Content-Type header.
    • Which converter function is used to turn your struct/array into a wire format.

5.2. Body converters

Converters are registered globally:

_elements_request_body_set_converter("application/json", function(value) {
    return json_encode(value);
});

At send time, _process_body does:

var _body_converter = _elements_request_body_get_converter(_content_type);
if (!is_callable(_body_converter)) {
    show_error(_where + " :: No converter for '" + _content_type + "'.", true);
}

_body = _body_converter(_body); // must be string or buffer

This means:

  • For application/json:

    • You’re expected to have a converter that json_encodes your struct/array.
  • For other content types:

    • You can define your own converters (e.g. multipart, binary, etc.).
  • If you pass a content type that has no converter registered:

    • The request hard-errors (show_error).

5.3. Example: overriding content_type

For the product bundle endpoint:

var bundles = [ /* array of ElementsProductBundle structs */ ];

// Let’s say your converter expects JSON when content_type is "application/json"
elements_update_product_bundle_for_application_configuration(
    "my-app",
    "my-config",
    bundles,
    "application/json",
    function(_code, _data, _request) { ... }
);

If you pass "*/*" and no converter is registered for "*/*", you’ll get an error. So in practice, you nearly always want "application/json" unless you’ve set up something custom.


6. Using the schema structs

The ElementsXXX constructors (ElementsItem, ElementsUser, ElementsRewardIssuance, etc.) are data containers. Each of them has a corresponding ElementsXXX_validate that can be used for validation.

Example:

var _user = new ElementsUser("user-id", "USER", "username");
var _item = new ElementsItem("sword", "Sword of Testing", "A test sword.", "FUNGIBLE");

var _issuance = new ElementsRewardIssuance(
    "reward-id",
    _user,
    "ISSUED",
    "my-context",
    "NON_PERSISTENT",
    _item,
    10
);

// Before sending:
ElementsRewardIssuance_validate(_issuance, _GMFUNCTION_); // endpoints do this for you

Key points:

  • ElementsXXX_validate() checks all fields and will show_error if something is wrong (wrong type, missing required fields).

The user of the API mostly needs to know:

  • You don’t need to call ElementsXXX_validate() yourself – the endpoint does it – but you can call it early to fail fast.

7. Retrying requests

ElementsRequest tracks how many times it was sent:

attempts = 0;
static retry = function() { return send(); }
  • The send() method increments attempts each time it’s called.
  • If you keep a reference to _request (e.g. via _request in your callback), you can manually call _request.retry() to resend it.
  • This is useful if you want a per-request retry after a specific error, rather than a global hook.

8. Putting it all together – typical usage flow

  1. Login / acquire tokens

    elements_create_username_password_session(_session_request, function(_code, _data, _request) {
        if (_code == 200) {
            var _session_secret = _data.sessionSecret;
            _elements_request_auth_set_token("auth_bearer", _session_secret);
            _elements_request_auth_set_token("session_secret", _session_secret);
        }
    });
  2. Optionally set global response hooks

    _elements_request_response_set_hook(401, function(_code, _data, _request) {
        show_debug_message("Unauthorized, redirecting to login...");
        // change room, clear saved session, whatever
        return true; // don’t call the per-request callback
    });
  3. Call other endpoints normally

    elements_get_application_profiles("my-app", 0, 20, "", function(_code, _data, _request) {
        if (_code == 200) {
            // use _data (already JSON-parsed)
        } else {
            // handle error
        }
    });
  4. When sending bodies, use the right struct types + content_type

    var _receipt = new ElementsCreateAppleIapReceipt(receipt_base64, "PRODUCTION");
    elements_upload_apple_iap_receipt(_receipt, function(_code, _data, _request) {
        // ...
    });

Clone this wiki locally