# Configuration (/docs/orbit-studios-resources/orbit-craftingsystem/configuration)



# Configuration [#configuration]

`orbit-craftingsystem` has two public config files. Use `shared/config.lua` for gameplay and recipes. Use `server/config.lua` for server-only logging, image paths, and Discord webhooks.

<Callout type="warn" title="Do not mix the files">
  Put craftable items, benches, stations, blueprints, XP, and access rules in `shared/config.lua`. Put webhook URLs and logging callbacks in `server/config.lua`.
</Callout>

## File Map [#file-map]

<Files>
  <Folder name="resources">
    <Folder name="[orbit]">
      <Folder name="orbit-craftingsystem">
        <Folder name="shared">
          <File name="config.lua" />
        </Folder>

        <Folder name="server">
          <File name="config.lua" />
        </Folder>
      </Folder>
    </Folder>
  </Folder>
</Files>

<TypeTable
  type="{
  'shared/config.lua': {
    type: 'gameplay config',
    description: 'Developer mode, crafting odds, XP, stations, benches, blueprint recipes, custom blueprint item names, target data, and access groups.',
  },
  'server/config.lua': {
    type: 'server config',
    description: 'Custom inventory image URL, Discord webhook URL, webhook sender, crafting log callback, and suspicious activity callback.',
  },
}"
/>

## Setup Workflow [#setup-workflow]

<Steps>
  <Step>
    ### Choose where the item belongs [#choose-where-the-item-belongs]

    Use a station item when the recipe should be available at a fixed world location. Use a bench non-blueprint item when the recipe should always be available on a portable bench. Use a blueprint item when players must own or install a blueprint first.
  </Step>

  <Step>
    ### Add the inventory items first [#add-the-inventory-items-first]

    Every final item and every required material must exist in your inventory or framework item list before testing. If the inventory does not know `metalscrap`, `steel`, or `weapon_pistol`, the crafting system cannot add or remove them correctly.
  </Step>

  <Step>
    ### Add one recipe and test it [#add-one-recipe-and-test-it]

    Add one recipe to `shared/config.lua`, restart the resource, open the station or bench, and test one craft. After that works, duplicate the same structure for the rest of your recipes.
  </Step>

  <Step>
    ### Enable logging [#enable-logging]

    Add your Discord webhook URL in `server/config.lua`, then keep or customize `Config.Server.OnCraft` and `Config.Server.SuspiciousActivity`.
  </Step>
</Steps>

## Shared Config Shape [#shared-config-shape]

`shared/config.lua` is read in this order. Keeping the same order makes it much easier to compare your file with the shipped config when you update the resource.

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config = Config or {}

Config.DeveloperMode = false
Config.CraftingWithWheel = true
Config.NuiState = function(bool)
    if not Config.DeveloperMode then return end
    print("NUI Open:", bool)
end

Config.Celebration = {
    animDict = "anim@mp_player_intcelebrationmale@air_guitar",
    animName = "air_guitar"
}

Config.BenchAnimations = {
    putting_down_bench = {
        duration = 5000,
        animDict = "amb@world_human_hammering@male@base",
        anim = "base",
        flag = 1
    },
    moving_bench = {
        duration = 1000,
        animDict = "amb@world_human_hammering@male@base",
        anim = "base",
        flag = 1
    }
}

Config.LevelValues = {
    xpGainMultiplier = 10,
    levelUpXPMultiplier = 100,
    loseMultiplier = 0.5
}

Config.MainRoutingBucket = 0
Config.Benches = {}
Config.Blueprints = {}
Config.CustomBlueprints = {}
Config.Stations = {}
```

The empty tables above are only an outline. In your real file, `Config.Benches`, `Config.Blueprints`, and `Config.Stations` contain the actual bench types, blueprint recipes, fixed stations, recipes, targets, and access groups.

## Add Bench Items To Inventory [#add-bench-items-to-inventory]

Every key inside `Config.Benches.benchTypes` must also exist as an inventory item. The default config includes `weapon-crafting_bench` and `medical_bench`, so those item names need to be added before players can place those benches.

<Callout type="warn" title="Bench item names must match">
  If the bench type is `["weapon-crafting_bench"]`, the inventory item must also be `weapon-crafting_bench`. Bench item names cannot start with `weapon_` and must include `bench`.
</Callout>

<Tabs groupId="framework" items="['ESX', 'QB', 'QBX']">
  <Tab value="ESX">
    Add the bench item inside your inventory resource. For ESX servers using `ox_inventory`, this is usually `ox_inventory/data/items.lua`.

    ```lua title="resources/[standalone]/ox_inventory/data/items.lua"
    ["weapon-crafting_bench"] = {
        label = "Weapon Crafting Bench",
        weight = 1000,
        stack = false,
        close = true,
        description = "Places a weapon crafting bench."
    },

    ["medical_bench"] = {
        label = "Medical Crafting Bench",
        weight = 1000,
        stack = false,
        close = true,
        description = "Places a medical crafting bench."
    },
    ```

    If your ESX server uses a different inventory, add the same item names using that inventory's item format. The important part is that the item name exactly matches the bench key in `Config.Benches.benchTypes`.
  </Tab>

  <Tab value="QB">
    Add the bench item in QBCore's shared item file, not inside the crafting resource.

    ```lua title="resources/[qb]/qb-core/shared/items.lua"
    ["weapon-crafting_bench"] = {
        name = "weapon-crafting_bench",
        label = "Weapon Crafting Bench",
        weight = 1000,
        type = "item",
        image = "weapon-crafting_bench.png",
        unique = true,
        useable = true,
        shouldClose = true,
        combinable = nil,
        description = "Places a weapon crafting bench."
    },

    ["medical_bench"] = {
        name = "medical_bench",
        label = "Medical Crafting Bench",
        weight = 1000,
        type = "item",
        image = "medical_bench.png",
        unique = true,
        useable = true,
        shouldClose = true,
        combinable = nil,
        description = "Places a medical crafting bench."
    },
    ```

    `unique = true` is recommended because benches can carry metadata, especially when `Config.Blueprints.store = "bench"`.
  </Tab>

  <Tab value="QBX">
    Add the bench item inside your inventory resource. For most Qbox/QBX servers this is the inventory item file, for example `ox_inventory/data/items.lua`.

    ```lua title="resources/[standalone]/ox_inventory/data/items.lua"
    ["weapon-crafting_bench"] = {
        label = "Weapon Crafting Bench",
        weight = 1000,
        stack = false,
        close = true,
        description = "Places a weapon crafting bench."
    },

    ["medical_bench"] = {
        label = "Medical Crafting Bench",
        weight = 1000,
        stack = false,
        close = true,
        description = "Places a medical crafting bench."
    },
    ```

    If you rename or add a bench type in `Config.Benches.benchTypes`, add the same item name here too.
  </Tab>
</Tabs>

The crafting system registers each bench type as a usable item through `orbit-lib`. That means players use the inventory item, the resource previews the bench prop, and then the bench is placed if the player confirms the position.

## Where To Add Items [#where-to-add-items]

Craftable items can live in three places. The recipe shape is mostly the same, but the location decides how the player accesses it.

<Tabs items="['Station items', 'Bench items', 'Blueprint items', 'Custom blueprint item names']">
  <Tab value="Station items">
    Put fixed-location recipes inside `Config.Stations[stationId].items`.

    ```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
    Config.Stations = {
        HideStationDistance = 50,

        ["weapon_station"] = {
            title = "Weapon Crafting Station",
            secondaryTitle = "Orbit Studios",
            blip = {
                haveBlip = true,
                sprite = 110,
                color = 1,
                scale = 0.8,
                coords = vector3(502.64, -1338.17, 29.31)
            },
            target = {
                type = "npc",
                model = "s_m_m_ammucountry",
                coords = vector3(502.64, -1338.17, 29.31),
                heading = 37.18
            },
            groups = {
                jobs = {},
                gangs = {}
            },
            items = {
                ["weapon_pistol"] = {
                    type = "weapon",
                    itemLevel = 1,
                    baseOdds = 50,
                    maxOdds = 90,
                    amountToCraft = 1,
                    requirements = {
                        { item = "plastic", amount = 5, removeOnSuccess = true, removeOnFail = true },
                        { item = "iron", amount = 5, removeOnSuccess = true, removeOnFail = false }
                    }
                }
            }
        }
    }
    ```

    Leave `groups.jobs` and `groups.gangs` empty for a public station. Add job or gang names only when the station should be restricted.
  </Tab>

  <Tab value="Bench items">
    Put bench recipes that do not need blueprints inside `Config.Benches.benchTypes[benchItemName].nonBlueprintItems`.

    ```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
    Config.Benches = {
        onlyOwnerCanMove = true,
        enableBenches = true,
        enablePersistentBenches = true,
        HideBenchDistance = 50,
        benchTypes = {
            ["weapon-crafting_bench"] = {
                benchProp = "prop_tool_bench02",
                benchLabel = "Weapon Bench",
                title = "Weapon Crafting Bench",
                secondaryTitle = "Orbit Studios",
                nonBlueprintItems = {
                    ["weapon_knife"] = {
                        type = "weapon",
                        itemLevel = 1,
                        baseOdds = 50,
                        maxOdds = 90,
                        amountToCraft = 1,
                        requirements = {
                            { item = "metalscrap", amount = 10, removeOnSuccess = true, removeOnFail = true },
                            { item = "steel", amount = 5, removeOnSuccess = true, removeOnFail = false }
                        }
                    }
                }
            }
        }
    }
    ```

    The bench type key, such as `weapon-crafting_bench`, must match the bench inventory item name. Bench item names cannot start with `weapon_` and must include `bench`.
  </Tab>

  <Tab value="Blueprint items">
    Put blueprint-locked recipes inside `Config.Blueprints.items`.

    ```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
    Config.Blueprints = {
        store = "bench",
        items = {
            ["weapon_pistol"] = {
                bench = "weapon-crafting_bench",
                type = "weapon",
                itemLevel = 1,
                baseOdds = 60,
                maxOdds = 90,
                amountToCraft = 1,
                requirements = {
                    { item = "metalscrap", amount = 10, removeOnSuccess = true, removeOnFail = true },
                    { item = "steel", amount = 5, removeOnSuccess = true, removeOnFail = false }
                }
            }
        }
    }
    ```

    `store = "bench"` stores learned blueprints on the bench. `store = "player"` stores learned blueprints on the player. The `bench` value must match a key in `Config.Benches.benchTypes`.
  </Tab>

  <Tab value="Custom blueprint item names">
    Use `Config.CustomBlueprints` when the physical inventory item has a different name from the recipe key.

    ```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
    Config.Blueprints = {
        store = "bench",
        items = {
            ["weapon_pistol"] = {
                bench = "weapon-crafting_bench",
                type = "weapon",
                itemLevel = 1,
                baseOdds = 60,
                maxOdds = 90,
                requirements = {
                    { item = "metalscrap", amount = 10, removeOnSuccess = true, removeOnFail = true }
                }
            }
        }
    }

    Config.CustomBlueprints = {
        ["weapon_pistol"] = {
            customBlueprintName = "blueprint_weapon_pistol"
        }
    }
    ```

    In this example, the blueprint content is still `weapon_pistol`, but the inventory item given to the player is `blueprint_weapon_pistol`.
  </Tab>
</Tabs>

## Recipe Fields [#recipe-fields]

Every recipe uses the same core fields whether it is placed in a station, bench, or blueprint list.

<TypeTable
  type="{
  type: {
    type: 'string | nil',
    description: 'Use &#x22;weapon&#x22; for weapon items. For normal items, this can usually be omitted.',
  },
  itemLevel: {
    type: 'number',
    description: 'Level associated with the recipe. If the crafting wheel is disabled, players below this level cannot craft the item.',
  },
  baseOdds: {
    type: 'number',
    description: 'Starting success chance at the item level. Example: 60 means 60 percent.',
  },
  maxOdds: {
    type: 'number',
    description: 'Maximum success chance the item can reach after level scaling.',
  },
  amountToCraft: {
    type: 'number',
    default: '1',
    description: 'How many final items are given when the craft succeeds.',
  },
  requirements: {
    type: 'Array<{ item: string, amount: number, removeOnSuccess?: boolean, removeOnFail?: boolean }>',
    description: 'Materials required for the craft. Each material item must exist in your inventory or framework item list.',
  },
}"
/>

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
["firstaid"] = {
    itemLevel = 2,
    baseOdds = 40,
    maxOdds = 90,
    amountToCraft = 2,
    requirements = {
        { item = "bandage", amount = 5, removeOnSuccess = true, removeOnFail = true },
        { item = "painkillers", amount = 2, removeOnSuccess = true, removeOnFail = true }
    }
}
```

Set `removeOnSuccess` and `removeOnFail` explicitly for each requirement while learning the config. That makes it obvious whether materials disappear on a successful craft, a failed craft, or both.

## Core Shared Options [#core-shared-options]

<TypeTable
  type="{
  'Config.DeveloperMode': {
    type: 'boolean',
    default: 'false',
    description: 'Enables debug prints and the /createblueprint command. Turn it off again on production servers unless you need it.',
  },
  'Config.CraftingWithWheel': {
    type: 'boolean',
    default: 'true',
    description: 'Enables the success-percentage crafting wheel. When false, crafting becomes simpler level gating.',
  },
  'Config.NuiState': {
    type: 'nil | function(bool)',
    description: 'Optional callback called when the crafting UI opens or closes.',
  },
  'Config.Celebration': {
    type: '{ animDict: string | nil, animName: string | nil }',
    description: 'Animation played after a successful craft. Set values to nil to disable it.',
  },
  'Config.BenchAnimations': {
    type: '{ putting_down_bench: ProgressAnim, moving_bench: ProgressAnim }',
    description: 'Progress duration and animation data used when placing or moving portable benches.',
  },
  'Config.LevelValues': {
    type: '{ xpGainMultiplier: number, levelUpXPMultiplier: number, loseMultiplier: number }',
    description: 'Controls XP gained, XP needed to level up, and XP given when crafting fails.',
  },
  'Config.MainRoutingBucket': {
    type: 'number',
    default: '0',
    description: 'Leave as 0 unless your server uses a different main routing bucket.',
  },
}"
/>

### Crafting Wheel [#crafting-wheel]

`Config.CraftingWithWheel` decides how strict the crafting level system is.

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config.CraftingWithWheel = true
```

When this is `true`, players use the success-percentage crafting wheel. `itemLevel`, `baseOdds`, and `maxOdds` decide how difficult the craft is and how much the chance can improve.

When this is `false`, crafting becomes level-gated. Players can craft items at their level or below their level, and the percentage wheel is not used.

### NUI State Callback [#nui-state-callback]

`Config.NuiState` runs whenever the crafting UI opens or closes. Use it when another resource needs to know that the player is inside the crafting interface.

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config.NuiState = function(bool)
    if not Config.DeveloperMode then return end
    print("NUI Open:", bool)
end
```

`bool = true` means the UI opened. `bool = false` means it closed.

Set it to `nil` if you do not need this callback:

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config.NuiState = nil
```

### Celebration Animation [#celebration-animation]

`Config.Celebration` is the animation played after a successful craft.

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config.Celebration = {
    animDict = "anim@mp_player_intcelebrationmale@air_guitar",
    animName = "air_guitar"
}
```

To change the animation, replace both values with a valid GTA animation dictionary and animation name. To disable the celebration, set both values to `nil`.

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config.Celebration = {
    animDict = nil,
    animName = nil
}
```

### Bench Animations [#bench-animations]

`Config.BenchAnimations` is used by the `orbit-lib` progress integration when a player places or moves a bench.

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config.BenchAnimations = {
    putting_down_bench = {
        duration = 5000,
        animDict = "amb@world_human_hammering@male@base",
        anim = "base",
        flag = 1
    },
    moving_bench = {
        duration = 1000,
        animDict = "amb@world_human_hammering@male@base",
        anim = "base",
        flag = 1
    }
}
```

`duration` is in milliseconds. `animDict`, `anim`, and `flag` are passed to the progress animation. If you make the duration very short, bench placement will feel instant and players may not understand that the bench is being placed.

### Level Values [#level-values]

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
Config.LevelValues = {
    xpGainMultiplier = 10,
    levelUpXPMultiplier = 100,
    loseMultiplier = 0.5
}
```

`loseMultiplier = 0.5` means a failed craft gives half of the XP the craft would normally give.

## Bench Placement [#bench-placement]

Bench placement controls where portable benches can be placed.

<Tabs items="['Blacklist', 'Whitelist', 'Zone shapes']">
  <Tab value="Blacklist">
    Blacklist mode allows bench placement everywhere except inside listed zones.

    ```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
    Config.Benches.benchPlacement = {
        enabled = true,
        mode = "blacklist",
        zones = {
            {
                vectors = {
                    vector3(494.22, -1328.00, 29.00),
                    vector3(494.70, -1333.96, 29.00),
                    vector3(500.11, -1335.09, 29.00)
                },
                height = 10
            }
        }
    }
    ```
  </Tab>

  <Tab value="Whitelist">
    Whitelist mode blocks bench placement everywhere except inside listed zones.

    ```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
    Config.Benches.benchPlacement = {
        enabled = true,
        mode = "whitelist",
        zones = {
            {
                vectors = {
                    vector3(493.93, -1335.25, 29.00)
                },
                radius = 5.0
            }
        }
    }
    ```
  </Tab>

  <Tab value="Zone shapes">
    Use `vectors` plus `height` for polygon zones. Use `vectors` plus `radius` for sphere zones. Use `vectors` plus `size = vec3(...)` for box zones.

    ```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
    {
        vectors = {
            vector3(493.93, -1335.25, 29.00)
        },
        size = vec3(2, 2, 2)
    }
    ```
  </Tab>
</Tabs>

## Station Access [#station-access]

Access groups live inside each station. Leave both tables empty for public use.

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
groups = {
    jobs = {},
    gangs = {}
}
```

Restrict by job or gang name with a minimum grade:

```lua title="resources/[orbit]/orbit-craftingsystem/shared/config.lua"
groups = {
    jobs = {
        ["police"] = 0,
        ["ambulance"] = 2
    },
    gangs = {
        ["lostmc"] = 1
    }
}
```

`["ambulance"] = 2` means the player must have the ambulance job at grade 2 or higher. Job and gang names must match the data returned by your framework through `orbit-lib`.

## Server Config [#server-config]

`resources/[orbit]/orbit-craftingsystem/server/config.lua` is server-only. This is where inventory image URLs, Discord webhooks, and logging callbacks live.

<TypeTable
  type="{
  'Config.Server.CustomImageURL': {
    type: 'false | string',
    default: 'false',
    description: 'Custom inventory image directory. Use this when item images are not found by the default path.',
  },
  'Config.Server.Webhook.url': {
    type: 'string',
    description: 'Discord webhook URL used by Config.Server.SendWebhook.',
  },
  'Config.Server.Webhook.username': {
    type: 'string | nil',
    description: 'Optional username shown on Discord webhook messages.',
  },
  'Config.Server.Webhook.avatar': {
    type: 'string | nil',
    description: 'Optional avatar URL shown on Discord webhook messages.',
  },
  'Config.Server.OnCraft': {
    type: 'nil | function(source, stationId, itemName, craftedAmount, itemLevel, isWin)',
    description: 'Called after a crafting attempt so you can log success or failure.',
  },
  'Config.Server.SuspiciousActivity': {
    type: 'nil | function(source, stationId, itemName, craftedAmount, reason, isWin)',
    description: 'Called when the server rejects an invalid or suspicious crafting attempt.',
  },
}"
/>

## Inventory Image URLs [#inventory-image-urls]

Use `Config.Server.CustomImageURL` when item images are missing in the crafting UI.

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.CustomImageURL = false
```

For `ox_inventory`, this is a common value:

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.CustomImageURL = "cfx-nui-ox_inventory/web/images/"
```

The image file name should match the inventory item name. For example, `weapon_pistol` normally needs an image named like `weapon_pistol.png` in the configured image directory.

## Webhooks [#webhooks]

Add your Discord webhook URL in `server/config.lua`. Do not put webhook URLs in `shared/config.lua`, because shared files can be loaded client-side.

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.Webhook = {
    url = "https://discord.com/api/webhooks/...",
    username = nil,
    avatar = nil
}
```

Use `username` and `avatar` only if you want to override the Discord webhook defaults:

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.Webhook = {
    url = "https://discord.com/api/webhooks/...",
    username = "Orbit Crafting Logs",
    avatar = "https://example.com/logo.png"
}
```

The included sender uses Discord embeds:

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
function Config.Server.SendWebhook(embed)
    local webhook = Config.Server.Webhook
    if not webhook.url or webhook.url == "" then return end

    PerformHttpRequest(
        webhook.url,
        function() end,
        "POST",
        json.encode({
            username = webhook.username,
            avatar_url = webhook.avatar,
            embeds = { embed }
        }),
        { ["Content-Type"] = "application/json" }
    )
end
```

## Crafting Log Webhook [#crafting-log-webhook]

`Config.Server.OnCraft` runs after a valid crafting attempt. It receives the player, station or bench ID, item name, crafted amount, item level, and whether the craft succeeded.

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.Colors = {
    success = "#2ecc71",
    failed = "#e74c3c",
    suspicious = "#ff0000"
}

local function HexToDecimal(hex)
    return tonumber(hex:gsub("#", ""), 16)
end

Config.Server.OnCraft = function(source, stationId, itemName, craftedAmount, itemLevel, isWin)
    local playerName = GetPlayerName(source) or "Unknown"
    local playerId = source

    local embed = {
        title = isWin and "Crafting Successful" or "Crafting Failed",
        description = isWin and "The player successfully crafted an item." or "The crafting attempt failed.",
        color = HexToDecimal(isWin and Config.Server.Colors.success or Config.Server.Colors.failed),
        author = {
            name = playerName .. " (" .. playerId .. ")"
        },
        fields = {
            {
                name = "Station or Bench",
                value = tostring(stationId),
                inline = false
            },
            {
                name = "Item",
                value = tostring(itemName),
                inline = true
            },
            {
                name = "Amount",
                value = tostring(craftedAmount),
                inline = true
            },
            {
                name = "Item Level",
                value = tostring(itemLevel),
                inline = true
            }
        },
        footer = {
            text = "Orbit Crafting System"
        },
        timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ")
    }

    Config.Server.SendWebhook(embed)
end
```

Set `Config.Server.OnCraft = nil` if you want to disable normal crafting logs entirely:

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.OnCraft = nil
```

## Suspicious Activity Webhook [#suspicious-activity-webhook]

`Config.Server.SuspiciousActivity` runs when the server detects an invalid crafting attempt. This is useful for exploit attempts, invalid items, missing requirements, or other rejected craft requests.

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.SuspiciousActivity = function(source, stationId, itemName, craftedAmount, reason, isWin)
    local playerName = GetPlayerName(source) or "Unknown"
    local playerId = source

    local embed = {
        title = "Suspicious Crafting Activity",
        description = "Potential exploit or invalid crafting attempt detected.",
        color = HexToDecimal(Config.Server.Colors.suspicious),
        author = {
            name = playerName .. " (" .. playerId .. ")"
        },
        fields = {
            {
                name = "Station or Bench",
                value = tostring(stationId),
                inline = false
            },
            {
                name = "Item",
                value = tostring(itemName),
                inline = true
            },
            {
                name = "Attempted Amount",
                value = tostring(craftedAmount),
                inline = true
            },
            {
                name = "Reason",
                value = tostring(reason),
                inline = false
            }
        },
        footer = {
            text = "Orbit Crafting System - monitor recommended"
        },
        timestamp = os.date("!%Y-%m-%dT%H:%M:%SZ")
    }

    Config.Server.SendWebhook(embed)
end
```

Set `Config.Server.SuspiciousActivity = nil` if you want to disable suspicious activity logs:

```lua title="resources/[orbit]/orbit-craftingsystem/server/config.lua"
Config.Server.SuspiciousActivity = nil
```

## Webhook Testing [#webhook-testing]

<Steps>
  <Step>
    ### Add the webhook URL [#add-the-webhook-url]

    Paste the Discord webhook URL into `Config.Server.Webhook.url` in `server/config.lua`.
  </Step>

  <Step>
    ### Keep `OnCraft` enabled [#keep-oncraft-enabled]

    Make sure `Config.Server.OnCraft` is a function, not `nil`.
  </Step>

  <Step>
    ### Craft one test item [#craft-one-test-item]

    Join the server, open a station or bench, and craft an item. Discord should receive a success or failure embed.
  </Step>

  <Step>
    ### Check the server console [#check-the-server-console]

    If no message arrives, confirm the URL is not empty, outbound HTTP is allowed, and the config file is saved on the server.
  </Step>
</Steps>

## Common Mistakes [#common-mistakes]

<Accordions>
  <Accordion title="The item does not appear in the UI">
    Make sure the recipe is inside the correct table: `Config.Stations[stationId].items`, `Config.Benches.benchTypes[benchName].nonBlueprintItems`, or `Config.Blueprints.items`. Also confirm the player has access to the station or bench.
  </Accordion>

  <Accordion title="The item appears but crafting fails">
    Confirm the final item and every requirement item exists in your inventory or framework item list. Also check `removeOnSuccess` and `removeOnFail` so required materials are handled the way you expect.
  </Accordion>

  <Accordion title="The webhook does not send">
    Put the URL in `Config.Server.Webhook.url`, keep `Config.Server.OnCraft` or `Config.Server.SuspiciousActivity` enabled, and make sure the webhook URL is valid. Webhook code belongs in `server/config.lua`, not `shared/config.lua`.
  </Accordion>

  <Accordion title="Job or gang access does not work">
    Job and gang names must match the values returned by your framework through `orbit-lib`. Remove access groups temporarily to confirm the station itself works, then add restrictions back one at a time.
  </Accordion>
</Accordions>
