Xash exposes several APIs that allow you to integrate Xash with your own scripts and objects for your own use or for resale.
If you sell a product that uses any of the Xash APIs, please put the Xash Logo Xash logo on your product or product ad.

Xash Chat API

Xash (v2.0.0 or later) broadcasts information on chat channel 24011908, that devices in your club could listen to.

All the chat messages are structured like so:

{command}|{data...}|{signature}

{signature} is a signature to verify the authenticity of the message.
It serves two purposes: it makes sure that no unauthorized person (griefer) can mess with your device, and it makes sure that your device only reacts to messages originating from your club.

Title Message Description
New Visitors <visitors|{visitor1Uuid}|{visitor2Uuid}|...|{visitorNUuid}|{signature} A list of UUIDs of the newly arrived visitors.
Current BPM <bpm|{bpm}|{signature} The current BPM (beats per minute, tempo of music on your stream) is chatted. {bpm} is the BPM number. Note that for the BPM to be announced, you have to activate the BPM service subscription on your Xash dashboard.

The following example shows how to set up and handle messages and verify the signature:

handleCommand(list listData)
{
    string strCommand = llList2String(listData, 0);

    // TODO: add your logic to handle commands, e.g.:
    if (strCommand == "<bpm")
    {
        llSay(0, "BPM: " + llList2String(listData, 1));
        ...
    }
}


string PUBKEY_PREFIX = "pubkey:";

string g_strParcel;
key g_keyPublicKeyRequestId;
string g_strPendingKeyId;

integer verifyMessage(list listData)
{
    string strMessageWithoutSignature = llDumpList2String(llList2List(listData, 0, -3), "|") + "|" + g_strParcel + "|";
    string strSignature = llList2String(listData, -1);
    string strKeyId = llList2String(listData, -2);
    
    string strPublicKey = llLinksetDataRead(PUBKEY_PREFIX + strKeyId);
    if (strPublicKey == "")
    {
        if (strKeyId != g_strPendingKeyId)
        {
            g_strPendingKeyId = strKeyId;
            g_keyPublicKeyRequestId = llHTTPRequest("https://carlyletheassolutions.com/xash/backend/api.php?action=pubkey&keyid=" + strKeyId, [], "");
        }

        return FALSE;
    }
    else if (strPublicKey == "#INVALID")
        return FALSE;

    integer nTime = llGetUnixTime() / 5;
    if (llVerifyRSA(strPublicKey, strMessageWithoutSignature + (string) nTime, strSignature, "sha1"))
        return TRUE;

    return llVerifyRSA(strPublicKey, strMessageWithoutSignature + (string) (nTime - 1), strSignature, "sha1");
}

default
{
    state_entry()
    {
        g_strParcel = llList2String(llGetParcelDetails(llGetPos(), [PARCEL_DETAILS_ID]), 0);
        llListen(24011908, "", NULL_KEY, "");
    }

    listen(integer nChannel, string strName, key keyUUID, string strMessage)
    {
        list listData = llParseStringKeepNulls(strMessage, ["|"], []);
        if (verifyMessage(listData))
            handleCommand(listData);
    }

    http_response(key keyRequestId, integer nStatus, list listMetadata, string strBody)
    {
        if (keyRequestId == g_keyPublicKeyRequestId)
        {
            if (nStatus == 200)
                llLinksetDataWrite(PUBKEY_PREFIX + g_strPendingKeyId, strBody);
            else if (nStatus == 404)
                llLinksetDataWrite(PUBKEY_PREFIX + g_strPendingKeyId, "#INVALID");

            g_keyPublicKeyRequestId = NULL_KEY;
            g_strPendingKeyId = "";
        }
    }
}
        

Xash Web API

Xash has an API that allows you to query for publicly available information about your club.
The responses are JSON.
{clubCode} is the 8-character code of your club that you can find in the description of your Xash server object.

List Upcoming Events Request
https://carlyletheassolutions.com/xash/backend/api.php?action=get-events&club={clubCode}&scope={scope}
                    

{scope} is an optional time range filter to restrict the events. It can be one of

  • today (return today's events)
  • week (return the events up until one week from now)
  • month (return the events up until one month from now)

Example Response
[
  {
    "id": 123,
    "seriesId": 0,
    "title": "DJ Carly",
    "description": "Live mixed house set",
    "startTimestamp": 1769202000,
    "endTimestamp": 1769209200,
    "start": "2026-01-23 21:00:00",
    "end": "2026-01-23 23:00:00",
    "startSlt": "2026-01-23 13:00:00",
    "endSlt": "2026-01-23 15:00:00",
    "djUuid": "79937e6d-ece0-4250-89bc-abcacd8b39c5",
    "djName": "Carlyle Theas",
    "hostUuid": null,
    "hostName": null
  }
]
                    
  • start and end are UTC times
  • startSlt and endElt are the times in SLT
  • If the seriesId is not equal to 0, the event belongs to an event series
  • hostUuid and hostName are null if no host was assigned to the event
List Available Slots Calculates available time slots where no events are scheduled, from now until midnight SLT of the selected scope's end date.

Request
https://carlyletheassolutions.com/xash/backend/api.php?action=get-free-slots&club={clubCode}&scope={scope}
                    

{scope} is an optional time range filter to restrict the events. It can be one of

  • today (check for free slots until SLT midnight tonight)
  • week (check for free slots until one week from now)
  • month (check for free slots until one month from now)

Example Response
[
  {
    "start": "2026-02-05 14:00:00",
    "end": "2026-02-05 16:00:00",
    "startSlt": "2026-02-05 06:00:00",
    "endSlt": "2026-02-05 08:00:00",
    "startTimestamp": 1770300000,
    "endTimestamp": 1770307200,
    "duration": 120
  }
]
                    
  • start and end are UTC times
  • startSlt and endElt are the times in SLT
  • If the seriesId is not equal to 0, the event belongs to an event series
  • hostUuid and hostName are null if no host was assigned to the event

Tip Jar API

The tip jars offer a link_message API in case you want to customize or extend their functionality.

Within the link_message event of a script you can react to the following messages:

Message Description integer num string str key id
LOG_IN_REQUESTED Sent when the user clicks the "Log In" button in the menu. 31004 The UUID of the agent requesting the log in
LOG_OUT_REQUESTED Sent when the user clicks the "Log Out" button in the menu. 31005 The UUID of the agent requesting the log out
LOG_IN Sent when a log in or log out was approved and the user is actually logged in/out. 30007 The UUID of the agent logged in, or NULL_KEY on log outs
GOAL_PERCENTAGE_UPDATED Sent if the tip jar is set to be a goal tip jar, and the percentage of the goal reached was updated. 32001 The total amount tipped to this tip jar The goal amount

Using the LOG_IN event, you could, e.g., create a tip jar that follows the logged in person around and returns to a default place when the user logs out.
See below for an example script.

Using the GOAL_PERCENTAGE_UPDATED event, you could create a donation thermometer tip jar. You would calculate the percentage with
integer percentage = ((integer) str) * 100) / ((integer) ((string) id));

Constants

Copy the following constants for use in your own script:

LOG_IN_REQUESTED = 31004;
LOG_OUT_REQUESTED = 31005;
LOG_IN = 30007;
GOAL_PERCENTAGE_UPDATED = 32001;
        

Examples

Following Tip Jar

vector OFFSET_FROM_AVATAR_CENTER = <0, 0, 1.5>;

vector g_vecHomePosition;

moveTo(vector vecTarget)
{
    integer i = 0;
    for ( ; i < 10; ++i)
    {
        vector vecBefore = llGetPos();
        llSetPos(vecTarget);
        vector vecAfter = llGetPos();

        if (llVecDist(vecAfter, vecTarget) < 0.01 || vecBefore == vecAfter)
            return;
    }
}

default
{    
    state_entry()
    {
        llSetStatus(STATUS_PHYSICS, FALSE);
    }
    
    on_rez(integer nStartParam)
    {
        llResetScript();
    }

    link_message(integer nSenderNum, integer nNum, string strMsg, key keyUUID)
    {
        if (nNum == 30007)
        {
            if (keyUUID)
            {
                // an agent logged in
                g_vecHomePosition = llGetPos();
                llSensorRepeat("", keyUUID, AGENT, 20, PI, 1);
            }
            else
            {
                // the agent logged out
                llSensorRemove();
                moveTo(g_vecHomePosition);
            }
        }
    }
    
    sensor(integer nNum)
    {
        moveTo(llDetectedPos(0) + OFFSET_FROM_AVATAR_CENTER);
    }
    
    no_sensor()
    {
        // move to the home position if the logged in user has left
        llSensorRemove();
        moveTo(g_vecHomePosition);
    }
}            
            

Stripping Tip Jar

The following script is an example of how you can turn a tip jar into a stripping tip jar. It will strip you when a tip threshold has been reached, and if no more tips are added, will start re-dressing you again after a specific amount of time.

The tip jar plugin script only sends the stripping and dressing commands. For the stripping to work, you will need to attach an additional device, such as Wardrobe's StripMe (which is available to Wardrobe Premium users), or the (CTS) StripMe device.

///////////////////////////////////////////////////////////
// CONFIGURATION

// Set this to TRUE if the script is used in a Xash tip jar.
// Set to FALSE if used in a standalone tip jar. In this case
// it is assumed that the tips will always go to the owner,
// i.e., the owner of the tip jar will be stripped.
integer IS_XASH_TIPJAR = TRUE;

// The amount of tips that needs to be given before the next layer
// of clothes are stripped from the person logged into the tip jar
integer TIPS_TO_STRIP_NEXT_LAYER = 200;

// The number of seconds before a layer of clothing is put on again
integer REDRESS_TIMEMOUT_IN_SECONDS = 600;

// The message that is shown in local chat when stripping occurs.
// The display name of the logged in person will be prepended to the mesage.
string STRIP_ANNOUNCEMENT = "is getting naked! Keep the tips coming if you want to see more skin!";

// An optional UUID of a sound that will be played when stripping occurs.
string STRIP_SOUND_UUID = "";


///////////////////////////////////////////////////////////
// IMPLEMENTATION

key g_keyLoggedInAgent = NULL_KEY;
integer g_nTipsCollected = 0;
integer g_nPreviousStripAmount = 0;
integer g_nNumLayersToStrip = 0;
integer g_bCanStripMore = TRUE;
integer g_bCanDressMore = FALSE;
integer g_bIsStripping = FALSE;


strip()
{
    integer nTimeout = REDRESS_TIMEMOUT_IN_SECONDS;

    if (g_nNumLayersToStrip > 0)
    {
        if (g_bCanStripMore)
        {
            g_bIsStripping = TRUE;
            llRegionSay(-5928148, (string) g_keyLoggedInAgent + "|strip");
        }

        --g_nNumLayersToStrip;
        if (g_nNumLayersToStrip > 0)
            nTimeout = 10;
    }

    llSetTimerEvent(nTimeout);
}

dress()
{
    integer nTimeout = 0;

    if (g_bCanDressMore)
    {
        llRegionSay(-5928148, (string) g_keyLoggedInAgent + "|dress");
        nTimeout = REDRESS_TIMEMOUT_IN_SECONDS;
    }

    llSetTimerEvent(nTimeout);
}

reset()
{
    g_nTipsCollected = 0;
    g_nPreviousStripAmount = 0;
    g_nNumLayersToStrip = 0;
    g_bCanStripMore = TRUE;
    g_bCanDressMore = FALSE;
    g_bIsStripping = FALSE;
}

default
{    
    state_entry()
    {
        // if not a Xash tip jar, assume the tips always go
        // to the owner and the owner is always logged in
        if (!IS_XASH_TIPJAR)
            g_keyLoggedInAgent = llGetOwner();

        reset();
        llListen(-5928148, "", NULL_KEY, "");
    }
    
    on_rez(integer nStartParam)
    {
        llResetScript();
    }

    listen(integer nChannel, string strName, key keyUUID, string strMessage)
    {   
        if (strMessage == "fully-dressed")
            g_bCanDressMore = FALSE;
        if (strMessage == "fully-undressed" || strMessage == "naked")
            g_bCanStripMore = FALSE;

        if (strMessage == "OK" && g_bIsStripping)
        {
            g_bCanDressMore = TRUE;

            if (STRIP_ANNOUNCEMENT)
                llSay(0, "💋👙 " + llGetDisplayName(g_keyLoggedInAgent) + " " + STRIP_ANNOUNCEMENT + " 👙💋");

            if (STRIP_SOUND_UUID)
                llPlaySound(STRIP_SOUND_UUID, 1);
        }

        g_bIsStripping = FALSE;
    }    

    link_message(integer nSenderNum, integer nNum, string strMsg, key keyUUID)
    {
        if (nNum == 30007)
        {
            g_keyLoggedInAgent = keyUUID;
            if (keyUUID)
                reset();
        }
    }

    money(key keyUUID, integer nAmount)
    {
        g_nTipsCollected += nAmount;

        g_nNumLayersToStrip += (g_nTipsCollected - g_nPreviousStripAmount) / TIPS_TO_STRIP_NEXT_LAYER;
        g_nPreviousStripAmount += g_nNumLayersToStrip * TIPS_TO_STRIP_NEXT_LAYER;

        if (g_nNumLayersToStrip > 0)
            strip();
    }

    timer()
    {
        if (g_nNumLayersToStrip > 0)
            strip();
        else
            dress();
    }
}