Skip to content

JavaScript API Wrapper

  • Automatic CSRF handling — No need to manage tokens manually
  • Built-in spinner — Shows a loading indicator during requests
  • Less coupling — If the API changes, the wrapper absorbs more of that churn
  • Cleaner code — Less boilerplate for you

The bundled admin and Huraga themes already load the wrapper. For a custom theme, add this to your base layout before your theme JavaScript:

{{ 'js/fossbilling.js'|public_asset_url|script_tag }}
{{ 'js/api.js'|public_asset_url|script_tag }}
<script src="{{ 'build/js/mytheme.js'|asset_url }}"></script>

The script_tag filter adds a cache-busting hash and prevents the same script from being rendered more than once.

If your custom theme does not reuse the bundled theme JavaScript, initialize automatic API forms and links after the DOM loads:

document.addEventListener('DOMContentLoaded', function() {
API._apiForm();
API._apiLink();
});

FOSSBilling uses session-bound CSRF tokens for protected browser API calls. Browser-rendered Twig pages expose CSRFToken and set a fossbilling_csrf cookie. The submitted token must match the server-side session token.

Call typeCSRF requirement
Guest client login/signup and staff loginRequired; POST only, same-origin browser context.
Guest cart mutationsRequired; same-origin browser context.
Other guest API callsNot required.
client or admin API calls with the browser sessionRequired.
External client or admin API calls using an API keyNot required.

The JavaScript wrapper reads the fossbilling_csrf cookie and sends it automatically on same-origin requests, including protected guest calls. Load a FOSSBilling page first to establish the session and token. If you include a CSRFToken form field explicitly, use the current page's token. Guest authentication requires the token in the body or header, not the query string. See the API reference for the protected endpoints.

The wrapper follows this pattern:

API.{role}.{method}(endpoint, params, onSuccess, onError, showSpinner)
ParameterDescription
roleadmin, client, or guest
methodUse get or post for core routes; guest authentication requires post. Prefer post for mutations.
endpointThe API endpoint (e.g., "currency/get_pairs")
paramsRequest parameters (object)
onSuccessCallback function for successful responses
onErrorCallback function for errors. Receives { message, code }.
showSpinnerBoolean to show/hide the loading spinner (optional, default: true)
API.guest.get(
"currency/get_pairs",
{},
function(response) {
console.log("Currency pairs:", response);
},
function(error) {
console.error("Error:", error);
}
);
API.admin.post(
"client/get_list",
{ per_page: 10, page: 1 },
function(response) {
console.log("Clients:", response.list);
},
function(error) {
console.error("Failed to load clients:", error);
}
);
API.admin.post(
"client/get_list",
{},
function(response) {
console.log(response.list);
},
function(error) {
console.error(error.message);
},
false
);

The wrapper automatically creates a loader element for requests when the spinner is enabled. To use it, make sure your theme has styling for .spinner-border.

The wrapper creates and removes the spinner element automatically:

.spinner-border {
display: inline-block;
animation: spin 1s linear infinite;
}

Spinner example

Either:

  • Don't define a .spinner-border class in your CSS
  • Pass false as the fifth parameter to the API call like below:
API.admin.post('client/get_list', {}, onSuccess, onError, false);

The Twig helpers fb_api_form() and fb_api_link() render a data-fb-api attribute that the wrapper uses for automatic form and link handling.

Use api_url for the target endpoint:

<form action="{{ 'profile/update'|api_url }}" {{ fb_api_form({ message: 'Profile updated'|trans }) }}>
<input type="text" name="first_name" value="{{ profile.first_name }}">
<button type="submit">{{ 'Save'|trans }}</button>
</form>

fb_api_form() adds method="post" and data-fb-api. The wrapper serializes the form, adds the CSRF token automatically for session-authenticated calls, disables enabled buttons while the request is running, and shows errors with FOSSBilling.message().

For API links:

<a href="{{ 'client/delete'|api_url(query: { id: client.id }, role: 'admin') }}"
{{ fb_api_link({
modal: { type: 'confirm', title: 'Are you sure?'|trans },
redirect: 'client'|url
}) }}>
{{ 'Delete'|trans }}
</a>

The wrapper intercepts links with data-fb-api, sends a GET request to the link href, and then handles the configured follow-up action.

Supported data-fb-api options:

OptionApplies toDescription
messageForms and linksShow a success toast after the request.
redirectForms and linksRedirect to the given URL after success.
reloadForms and linksPass true to reload the current page after success.
callbackForms and linksName of a global window function to call with the API result.
paramsLinksExtra request parameters merged into the link request.
modalLinksShow a confirmation, danger, or prompt modal before the request.

Prompt modals require a key; the entered value is sent under that request parameter name:

<a href="{{ 'example/update_name'|api_url }}"
{{ fb_api_link({
modal: { type: 'prompt', title: 'New name'|trans, label: 'Name'|trans, key: 'name' },
reload: true
}) }}>
{{ 'Rename'|trans }}
</a>

The success callback receives the decoded API response object:

function(response) {
console.log(response);
}

The error callback receives the API error payload:

function(error) {
console.error(error.message, error.code);
}

The wrapper passes the API error's message and code to onError, including on non-200 responses. Custom wrappers must check both the HTTP status and the JSON error field.

Use manual fetch() only when the wrapper does not fit. Include the CSRF token for session-authenticated admin/client calls and protected guest authentication/cart calls. Use the JSON body, form data, or X-CSRF-Token header; query tokens are not accepted for guest authentication:

function getCsrfToken() {
const match = document.cookie.match(/fossbilling_csrf=([^;]*)/);
return match ? decodeURIComponent(match[1]) : '';
}
fetch('{{ "profile/update"|api_url }}', {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(),
},
body: JSON.stringify({ first_name: 'Jane' }),
});
function loadProfile() {
API.client.get(
"profile/get",
{},
function(profile) {
document.getElementById('name').textContent = profile.first_name + ' ' + profile.last_name;
document.getElementById('email').textContent = profile.email;
},
function(error) {
alert('Failed to load profile: ' + error.message);
}
);
}
document.addEventListener('DOMContentLoaded', loadProfile);

For more details, see the API Reference.