# 3rd party / external key check API

This documentation is for 3rd party non-lua related applications who wants to use the Luarmor Ad Reward system to generate & validate keys.

{% hint style="danger" %}
If you are a script developer, refer to[ this documentation](https://docs.luarmor.net/luarmor-user-manual-and-f.a.q#key-check-library) instead. **This page isn't for you**.
{% endhint %}

Your project must be approved before you can use any of these endpoints, if you don't have the "shared secrets" or "app name", contact federal.

### INTRO

This API is straightforward, you can check if a key is valid / banned / expired / hwid locked etc., the only complicated part is the generation of SHA1 signatures in request and response bodies. These SHA1 signatures are required in order to ensure that important parts of the HTTP traffic haven't been tampered with.

Once the user has generated / renewed a key via ad link, their key will have the `"reset"` state. In this state, first validity check request will automatically mark the key as "`claimed/HWID linked`" and checking it's validity from a different HWID will return a HWID mismatch error.

All activity on this API is reflected to the dashboard and keys as "execution count". You can see the statistics of daily executions, total users, who executed how many times etc.

### HTTP API

{% hint style="info" %}
You will make **2 GET requests** in total. \
\- First request is to fetch server time & endpoints list. \
\- Second request is to actually check the key. \
\
All requests must have **`Content-Type: application/json`** header and **`"GET"`** method.\
Your platform's User-Agent must be previously whitelisted by Luarmor. Contact federal for that.
{% endhint %}

#### STEP 1 - First Request (Fetch server info):

<mark style="color:green;">**`GET`**</mark> `https://sdkapi-public.luarmor.net/sync`

Response:&#x20;

```json
{
  st: 1739703913, // UNIX TIMESTAMP OF THE CLOUDFLARE WORKER
  cf: "AMS", // CF COL (REGION) NAME << not needed for the auth
  nodes: [ // available nodes that you can randomly pick from.
    "https://eu1-roblox-auth.luarmor.net/",
    "https://as1-roblox-auth.luarmor.net/",
    "https://as2-roblox-auth.luarmor.net/",
    "https://as3-roblox-auth.luarmor.net/",
    "https://us1-roblox-auth.luarmor.net/",
    "https://us2-roblox-auth.luarmor.net/",
    "https://au1-roblox-auth.luarmor.net/",
    "https://au2-roblox-auth.luarmor.net/"
  ]
}
```

Parse this json good and nice, pick a random node URL from the "nodes" array. Make sure to randomly pick it at runtime, so load is equally balanced.

**"st"** stands for "server time", you will use this value while calculating the "request signature". Keep it in a variable for now. It is always a **32 bit integer.**&#x20;

Your implementation should be looking like this so far:

{% tabs %}
{% tab title="NodeJS" %}
{% code overflow="wrap" %}

```javascript
const fetch = require('node-fetch');
const crypto = require('crypto');

const secret_n1 = "asdfdg**********"
const secret_n2 = "zxczxcv*********"
const secret_n3 = "hjgh************"
// You will be given 3 "shared secrets" by the owner, in DMs.
// you'll use them in the SHA1 signature calc in next step.

const app_name = "minecraftdlc" // you will be given this too, by federal.

let keyToCheck = "BAfjuLxndwTvMBNiCyqMsXMaTcOqXpcr" // user-inputted

// random str gen a-z A-Z and 0-9 only. And fixed 16 char output.
function randomString() {
    const length = 16
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let result = '';
    for (let i = 0; i < length; i++) {
        result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
}

function sha1Hash(data) { // SHA1 with LOWERCASE HEX output
    return crypto.createHash('sha1').update(data).digest('hex');
}

async function main() {
    const url1 = 'https://sdkapi-public.luarmor.net/sync';
    try {
        const response1 = await fetch(url1);
        const json1 = await response1.json();
        console.log(json1)

        const SERVER_TIME = json1.st;
        const NODES = json1.nodes;
        
        let randomNode = NODES[Math.floor(Math.random() * NODES.length)];

        console.log('Random node:', randomNode); 
        // e.g https://us2-roblox-auth.luarmor.net/
        
        // rest of the code will be written in step 2.
```

{% endcode %}
{% endtab %}

{% tab title="C++" %}

```cpp
#include <iostream>
#include <string>
#include <random>
#include <sstream>
#include <iomanip>
#include <vector>
#include <stdexcept>
#include <curl/curl.h>
#include <openssl/sha.h>
#include <nlohmann/json.hpp>

// convenience
using json = nlohmann::json;

const std::string secret_n1 = "asdfdg**********";
const std::string secret_n2 = "zxczxcv*********";
const std::string secret_n3 = "hjgh************";
const std::string app_name   = "minecraftdlc";  // provided by federal.
std::string keyToCheck = "BAfjuLxndwTvMBNiCyqMsXMaTcOqXpcr";

// a-z A-Z 0-9 x 16
std::string randomString() {
    const int length = 16;
    const std::string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    std::random_device rd;
    std::mt19937 generator(rd());
    std::uniform_int_distribution<> dist(0, chars.size() - 1);

    std::string result;
    for (int i = 0; i < length; ++i) {
        result.push_back(chars[dist(generator)]);
    }
    return result;
}

// sha1 lowercase
std::string sha1Hash(const std::string& data) {
    unsigned char hash[SHA_DIGEST_LENGTH];
    SHA1(reinterpret_cast<const unsigned char*>(data.c_str()), data.size(), hash);
    
    std::ostringstream oss;
    for (int i = 0; i < SHA_DIGEST_LENGTH; i++) {
        oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
    }
    return oss.str();
}

int main() {
    try {
        const std::string url1 = "https://sdkapi-public.luarmor.net/sync";
        std::string response = httpGet(url1); // can use a cURL wrapper here.

        // get JSON response
        json json1 = json::parse(response);
        std::cout << "JSON Response:\n" << json1.dump(4) << std::endl;

        if (!json1.contains("st") || !json1.contains("nodes")) {
            throw std::runtime_error("JSON does not contain required keys.");
        }
        auto SERVER_TIME = json1["st"].get<std::string>();
        auto nodesArray = json1["nodes"].get<std::vector<std::string>>();

        if (nodesArray.empty()) {
            throw std::runtime_error("No nodes found in JSON response.");
        }
        std::random_device rd;
        std::mt19937 gen(rd()); 
        std::uniform_int_distribution<> distr(0, nodesArray.size() - 1);
        std::string randomNode = nodesArray[distr(gen)];

        std::cout << "SERVER_TIME: " << SERVER_TIME << std::endl;
        std::cout << "Random node: " << randomNode << std::endl;
        
        // reset of the code will be given in the next step
```

{% endtab %}
{% endtabs %}

#### STEP 2 - Checking the key

<mark style="color:green;">**`GET`**</mark> `https://[node-name].luarmor.net/external_check_key?by=...&key=...`

<mark style="color:orange;">**`Query parameters:`**</mark>

**`by`:** Integration / app name. Defined as "app\_name" in the example code above.

**`key` :** User-inputted 32-char alphabetic (a-z A-Z) key to check the validity of.

<mark style="color:orange;">**`Headers:`**</mark>

| Header Name       | Header Value                             | Description                                                                             |
| ----------------- | ---------------------------------------- | --------------------------------------------------------------------------------------- |
| Content-Type      | application/json                         |                                                                                         |
| clienttime        | 1739703913                               | the unix timestamp retrieved from server.                                               |
| clientnonce       | s2mle100lesh420f                         | 16-char random string generated by your code.                                           |
| clienthwid        | 03b3b409-f0b97340-40b97304-48327b49827   | HWID value.                                                                             |
| exec-fingerprint  | 03b3b409-f0b97340-40b97304-48327b49827   | HWID value, same as clienthwid. But "exec-" must be the name of your platform/executor. |
| externalsignature | 0391a1e58f324b3a0c79d32dd09436bd45bfc773 | SHA1 signature. See below for how it's calculated.                                      |

{% hint style="info" %}
You must create a random nonce (16 char alphanumeric string) called **clientnonce**, which you'll later reference again to re-calculate server signature. For now, let's hold it in a variable.&#x20;
{% endhint %}

{% hint style="success" %}
**How is "externalsignature" calculated?**

{% code overflow="wrap" %}

```javascript
// combine them in this order:
sha1Hash(
    client_nonce + secret_n1 +
    keyToCheck + secret_n2 +
    SERVER_TIME + secret_n3 + 
    client_hwid
);

// example input to the hash function would be:
s2mle100lesh420fasdfdg**********BAfjuLxndwTvMBNiCyqMsXMaTcOqXpcrzxczxcv*********1739703913hjgh************03b3b409-f0b97340-40b97304-48327b49827

SHA1 Output:

28fc97338f74908528fbfdd5fc1cfc7ce313a017 a.k.a "externalsignature"
```

{% endcode %}
{% endhint %}

This ensures that the outgoing request parameters can't be spoofed / tampered with, without replicating the signature, which should be significantly difficult if you obfuscate/virtualize the auth part of your binary.

**HTTP Response would look like this:**

```json
{
  code: "KEY_VALID", // see below for all possible "code"s
  message: "The provided key is valid.", // reflect this to user
  data: { note: "Ad Reward", total_executions: 9, auth_expire: 1740394140 },
  signature: "b5c7a24c6c5c0558ee9d0a754a74a236d7270737"
}
```

{% hint style="danger" %}
⚠️You will get a **"signature"** field in the response if the code is "KEY\_VALID".&#x20;

\
For everything else, there will be no signature included. Just simply reflect the error message to the user. It doesn't matter if they spoof anything other than KEY\_VALID.\
\
See below for response signature validation.&#x20;
{% endhint %}

{% hint style="success" %}
**How is KEY\_VALID -> "signature" calculated?**

{% code overflow="wrap" %}

```javascript
// combine values in this order:
sha1Hash(
    client_nonce + secret_n3 + // s2mle100lesh420f + hjgh************
    json2.code // "KEY_VALID"
);

// example input to the hash function would be:
s2mle100lesh420fhjgh************KEY_VALID

SHA1 Output:

e9cd0cc2445374f5e0942f822892dca7e68df228 a.k.a response "signature"
```

{% endcode %}
{% endhint %}

You will use this response signature to check if returned KEY\_VALID is actually real, and not just coming from a skid's fiddler4 autorespond rule.

Rest of your code should look like this:

{% code overflow="wrap" %}

```javascript

const url2 = randomNode + 'external_check_key?by=' + app_name + '&key=' + keyToCheck;
let client_nonce = randomString(16);

let client_hwid = "0b4082374928374b2934792374-abcdef"
let extSignature = sha1Hash(client_nonce + secret_n1 + keyToCheck + secret_n2 + SERVER_TIME + secret_n3 + client_hwid);

console.log("Sending signature:", extSignature); // dont actually print in production

const customHeaders = {
    'Content-Type': 'application/json',
    'clienttime': SERVER_TIME,
    'externalsignature': extSignature,
    'clientnonce': client_nonce,
    'clienthwid': client_hwid,
    'executor-fingerprint': "0b4082374928374b2934792374-abcdef"
}
const response2 = await fetch(url2, {
    method: 'GET',
    headers: customHeaders
});

const json2 = await response2.json();
console.log('Response from GET:', json2);
// verifying the response authenticity
let server_nonce = json2.signature;
let serverSignature = sha1Hash(client_nonce + secret_n3 + json2.code);
if (json2.code === "KEY_VALID") {
    if (serverSignature !== server_nonce) {
        console.log('Server signature verification failed - tampered');
        return;
    } else {
        console.log('Server signature verification OK!!!');
        console.log("KEY is valid.")
        
    }
} else {
    console.log("Key verification failed: " + json2.code + ". Message: " + json2.message)
}
```

{% endcode %}

Possible status codes can be found here:

{% embed url="<https://docs.luarmor.net/luarmor-user-manual-and-f.a.q#possible-status-codes>" %}

But generally, you're only interested in whether it is "KEY\_VALID" or not.

Contact f.e.d.e.r.a.l for any questions.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.luarmor.net/docs-for-3rd-parties/3rd-party-external-key-check-api.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
