API reference
Soulbound: Online — public game data
Public, read-only game data for Soulbound: Online. No signup, no cost, no write path.
Quick start#
Everything is a GET. Nothing needs a key. Start with five legendary
chest pieces:
curl
curl -s "https://api.soulbound.tools/v1/items?slot=chest&rarity=legendary&limit=5"
JavaScript
const res = await fetch(
"https://api.soulbound.tools/v1/items?slot=chest&rarity=legendary&limit=5"
);
const { data, meta } = await res.json();
console.log(meta.total, data[0].name);Python
import json, urllib.request
url = "https://api.soulbound.tools/v1/items?slot=chest&rarity=legendary&limit=5"
with urllib.request.urlopen(url) as r:
body = json.load(r)
print(body["meta"]["total"], body["data"][0]["name"])Search by name — q is a case-insensitive substring, and every collection
takes it:
curl -s "https://api.soulbound.tools/v1/items?q=iron"
One resource by id:
curl -s "https://api.soulbound.tools/v1/items/accessory_t2_belt_001"
The artwork for whatever an object's icon field names:
curl -s -o icon.png "https://api.soulbound.tools/v1/img/belt_dark_2.png"
Worked example — every legendary chest piece with crit damage#
Stat keys are not guessable, so start from the vocabulary:
curl -s "https://api.soulbound.tools/v1/stats" | jq '.data[] | select(.name | test("crit"; "i"))'
That names the key you want — critical_strike_damage at the time this page was
rendered. Now filter the items by slot and rarity, and select on the stat
client-side: the API filters on the fields listed in the reference below, and
stats are an array on each item.
curl -s "https://api.soulbound.tools/v1/items?slot=chest&rarity=legendary&limit=200" \
| jq '[ .data[]
| select(any(.stats[]?; .stat == "critical_strike_damage"))
| { id, name,
crit: (.stats[] | select(.stat == "critical_strike_damage") | .value),
range: .roll.critical_strike_damage } ]'
If meta.cursor comes back, there is another page. Follow it until it does not:
cursor=""; while :; do
page=$(curl -s "https://api.soulbound.tools/v1/items?slot=chest&rarity=legendary&limit=200${cursor:+&cursor=$cursor}")
echo "$page" | jq -c '.data[] | select(any(.stats[]?; .stat == "critical_strike_damage")) | {id, name}'
cursor=$(echo "$page" | jq -r '.meta.cursor // empty')
[ -z "$cursor" ] && break
done
roll.critical_strike_damage is the
[min, max] the stat can roll on that item. Drop rates, loot-table weights and
loot-table composition are not published. effects[].chance is the chance an
effect fires in combat — it is not a drop rate.AI agents#
This API happens to be shaped the way an agent wants one: read-only, one error shape, cursor pagination, and a published machine-readable description. There is nothing to install and no connector to configure.
Point it at the spec#
The complete description of every route, parameter and response is an OpenAPI 3.1 document, generated from the same route table the Worker dispatches on. Most agent frameworks turn that into tool definitions directly.
https://api.soulbound.tools/v1/openapi.json OpenAPI 3.1, generated — cannot drift from what is served https://api.soulbound.tools/llms.txt the short version, in prose, for a model to read
A system prompt you can paste#
Most of the cost of pointing a model at an unfamiliar API is the requests it spends working out the rules. These are the rules:
You have access to the Soulbound: Online public game-data API, whose base
URL is https://api.soulbound.tools — a free, read-only source of game data.
- Every endpoint is GET and needs no authentication. There is no write path.
- Collections are cursor-paginated: pass meta.cursor back as ?cursor= to get
the next page, and stop when it is absent. limit defaults to 50, max 200.
- Filter with the documented query parameters; combine them with AND. q is a
case-insensitive substring match on name.
- Stat keys are not guessable. Read /v1/stats first, then use those keys.
- Errors are always {"error": {"code", "message"}}. On 429, wait for the
retry-after header rather than retrying immediately.
- Responses are cacheable for 300 seconds and carry an etag. Reuse them.
- Drop rates, drop chances, loot-table weights and loot-table composition are
NOT published and never will be. Do not go looking for them and do not
estimate them. (item.effects[].chance IS published — it is the chance an
effect fires in combat, not a drop rate.)
- The dataset is published by hand and can lag the live game by a patch.
/v1/meta carries the version and publish date.
Give a model one tool#
One tool over one GET is enough for most questions. Both examples use
the official Anthropic SDK and its tool runner, which drives the call loop for you.
Python
from anthropic import Anthropic, beta_tool
import json, urllib.parse, urllib.request
@beta_tool
def soulbound_get(path: str, query: str = "") -> str:
"""Fetch a path from the Soulbound: Online public API.
Args:
path: API path, for example /v1/items or /v1/stats
query: optional query string, for example slot=chest&rarity=legendary
"""
url = "https://api.soulbound.tools" + path + (("?" + query) if query else "")
req = urllib.request.Request(url, headers={"user-agent": "my-tool/1.0"})
with urllib.request.urlopen(req) as r:
return r.read().decode()
client = Anthropic()
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
system=SYSTEM_PROMPT, # the block above
tools=[soulbound_get],
messages=[{"role": "user",
"content": "Which legendary chest piece has the highest crit damage?"}],
)
for message in runner:
print(message)TypeScript
import Anthropic from "@anthropic-ai/sdk";
import { betaZodTool } from "@anthropic-ai/sdk/helpers/beta/zod";
import { z } from "zod";
const client = new Anthropic();
const soulboundGet = betaZodTool({
name: "soulbound_get",
description: "Fetch a path from the Soulbound: Online public API.",
inputSchema: z.object({
path: z.string().describe("API path, e.g. /v1/items"),
query: z.string().optional().describe("Query string, e.g. slot=chest"),
}),
run: async ({ path, query }) => {
const url = "https://api.soulbound.tools" + path + (query ? "?" + query : "");
const res = await fetch(url, { headers: { "user-agent": "my-tool/1.0" } });
return await res.text();
},
});
const message = await client.beta.messages.toolRunner({
model: "claude-opus-5",
max_tokens: 16000,
system: SYSTEM_PROMPT, // the block above
tools: [soulboundGet],
messages: [{ role: "user",
content: "Which legendary chest piece has the highest crit damage?" }],
});claude-opus-5 is a good default for open-ended questions over this data.
For plain lookups — resolve a name to an id, read one item — claude-haiku-4-5
is faster and cheaper and will not do a worse job.
If you are running an agent against this#
None of this is enforced. All of it makes you a good neighbour, and keeps you under the anonymous limit without trying.
- Send a descriptive
user-agent. If something you do looks like abuse, that string is the difference between us asking you about it and us blocking it. - Cache. Every response carries an
etagandx-dataset-version; re-fetching unchanged data is the single easiest way to burn your allowance. - Pull the collection once and query it locally rather than issuing one request per
item.
/v1/items?limit=200paginates through the whole set in a handful of calls. - Ask for a key in Discord if 60 requests a minute is genuinely not enough.
Conventions#
Response shapes#
A collection:
{ "data": [ … ], "meta": { "total": 1468, "count": 50, "cursor": "eyJvIjo1MH0" } }
A single resource:
{ "data": { … } }
An error, always this shape:
{ "error": { "code": "not_found", "message": "…" } }
Pagination#
limit defaults to 50 and is clamped to 200.
meta.cursor is opaque — pass it back as ?cursor= and change
nothing else. It is absent once the results are exhausted.
Methods#
GET and HEAD only. OPTIONS answers CORS preflight
with 204. Anything else is 405. There is no write path in this
API at all. Unknown query parameters are ignored, never an error.
Caching#
JSON is public, max-age=300. Artwork comes straight off static
assets and carries its own content etag; sprite ids are stable, so
cache it hard on your side. Every JSON response carries an etag and
the dataset version in x-dataset-version. Send the etag back to skip
the body:
curl -s -o /dev/null -w '%{http_code}\n' \
-H 'If-None-Match: "2026-08-07.1-…"' "https://api.soulbound.tools/v1/stats"
CORS#
access-control-allow-origin: * on everything. Browser JavaScript can read
etag, x-dataset-version and the ratelimit-* headers.
Errors#
Every failure is the same shape — { "error": { "code", "message" } } — so
you can branch on code and never parse a message.
| Code | HTTP | When |
|---|---|---|
not_found | 404 | No route matches, or no resource with that id. |
bad_request | 400 | A malformed limit, cursor or filter value. |
rate_limited | 429 | Over the tier limit. Back off until retry-after. |
method_not_allowed | 405 | Anything other than GET, HEAD or OPTIONS. |
internal | 500 | Something broke our end. Safe to retry. |
Messages are written by us and never echo your input back, so they are safe to log verbatim.
Rate limits#
| Tier | Limit | Keyed on |
|---|---|---|
anonymous | 60 / 60s | client IP |
community | 600 / 60s | key id |
images | 1200 / 60s | client IP |
Every response carries ratelimit-limit, ratelimit-remaining and
ratelimit-reset (seconds until the window rolls). A 429 adds
retry-after. ratelimit-remaining is a best-effort figure: the
limit is enforced across the edge, the header is what the responding instance has
counted, so treat it as a hint and back off on 429.
ratelimit-remaining
you read may have been counted for whoever warmed the cache. The 429 and its
retry-after are always fresh.API keys#
Optional. A key raises your rate limit and lets us tell one consumer from another. It unlocks no additional data — there is no gated tier.
curl -s -H "Authorization: Bearer sbt_live_7f3a2b_…" "https://api.soulbound.tools/v1/items"
?key= is also accepted, and discouraged: a key in a URL ends up in browser
history, in Referer headers, and in any log that records request lines.
Use the header wherever you can.
An unknown or revoked key is treated as anonymous rather than rejected. The data is public; a bad key should cost you your extra headroom, not your access.
Want one? Say what you are building and roughly where it will run, in Discord.
Versioning#
Two different things carry a version, and only one of them changes shape.
The /v1 path#
The API contract. A field will be added to a response without warning; a field will
not be removed or change meaning under /v1. Write your parser to ignore
what it does not recognise.
The dataset version#
The contents. It is on every response as x-dataset-version and in
/v1/meta as version, alongside the publish date and record
counts. It changes whenever we publish, which we do by hand — so the data can lag the
live game by a patch. Poll /v1/meta and compare version to
detect a change; it is a cheap request and it is the intended way to do this.
curl -s "https://api.soulbound.tools/v1/meta" | jq -r '.data.version, .data.published'
Reference#
Generated from the Worker's own route table. If a route is not here, the Worker does not serve it.
GET/#
Redirect to the documentation 302 to /docs.
Request
curl -sI "https://api.soulbound.tools/"
Responses
| Status | Meaning |
|---|---|
302 | Redirect to /docs. |
GET/docs#
Human-readable API reference A self-contained HTML page, rendered from this same spec.
Request
curl -s "https://api.soulbound.tools/docs"
Responses
| Status | Meaning |
|---|---|
200 | HTML reference. |
GET/llms.txt#
The API in prose, for a language model to read The llms.txt convention. Plain text, generated from this same route table. Says what the API is, what the rules are, and what is deliberately not published — build tools from the OpenAPI document, not from this.
Request
curl -s "https://api.soulbound.tools/llms.txt"
Responses
| Status | Meaning |
|---|---|
200 | Plain-text summary for language models. |
GET/v1/openapi.json#
This OpenAPI document Generated from the Worker's own route table, so it cannot drift from what is served.
Request
curl -s "https://api.soulbound.tools/v1/openapi.json"
Responses
| Status | Meaning |
|---|---|
200 | This document. |
GET/v1/items#
List items Every equippable and cosmetic item. Filters combine with AND.
Query parameters
| Parameter | Type | Meaning |
|---|---|---|
slot | string | Equipment slot, exact match, case-insensitive. |
rarity | string | Rarity, either numeric (4) or by name (legendary). |
subtype | string | Item subtype, exact match, case-insensitive. |
cosmetic | boolean | true or false. |
q | string | Case-insensitive substring match on name. |
limit | integer | Page size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200. |
cursor | string | Opaque cursor from a previous response's meta.cursor. |
Request
curl -s "https://api.soulbound.tools/v1/items?slot=chest&rarity=legendary"
Response
{
"data": [
{
"id": "accessory_t2_belt_001",
"name": "Iron Brawler's Belt",
"description": "A no-nonsense strap for fist-based problem solving. Found in Stable, Unstable and Fractured dungeon chests.",
"type": "belt",
"slot": "belt",
"rarity": 2,
"rarityName": "rare",
"cosmetic": false,
"icon": "belt_dark_2",
"stats": [
{
"stat": "power_physical",
"value": 10
}
],
"roll": {
"attack_speed": [
2,
2.5
],
"critical_strike_chance": [
2,
2.5
],
"critical_strike_damage": [
6,
9.5
],
"health": [
20,
25
],
"heavy_hit_chance": [
2,
2.35
],
"knockback": [
6,
10
]
},
"dungeons": [
"Arcadia Arena · Unstable",
"Arcadia Defence · Unstable"
]
}
],
"meta": {
"total": 1452,
"count": 1,
"cursor": "eyJvIjoxfQ"
}
}
Responses
| Status | Meaning |
|---|---|
200 | A page of results. |
400 | Malformed limit, cursor or filter value. |
429 | Rate limited. |
GET/v1/items/{id}#
Fetch one item
Request
curl -s "https://api.soulbound.tools/v1/items/accessory_t2_belt_001"
Response
{
"data": {
"id": "accessory_t2_belt_001",
"name": "Iron Brawler's Belt",
"description": "A no-nonsense strap for fist-based problem solving. Found in Stable, Unstable and Fractured dungeon chests.",
"type": "belt",
"slot": "belt",
"rarity": 2,
"rarityName": "rare",
"cosmetic": false,
"icon": "belt_dark_2",
"stats": [
{
"stat": "power_physical",
"value": 10
}
],
"roll": {
"attack_speed": [
2,
2.5
],
"critical_strike_chance": [
2,
2.5
],
"critical_strike_damage": [
6,
9.5
],
"health": [
20,
25
],
"heavy_hit_chance": [
2,
2.35
],
"knockback": [
6,
10
]
},
"dungeons": [
"Arcadia Arena · Unstable",
"Arcadia Defence · Unstable"
]
}
}
Responses
| Status | Meaning |
|---|---|
200 | The resource. |
404 | No such resource. |
429 | Rate limited. |
GET/v1/abilities#
List abilities
Query parameters
| Parameter | Type | Meaning |
|---|---|---|
category | string | Ability category, exact match, case-insensitive. |
q | string | Case-insensitive substring match on name. |
limit | integer | Page size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200. |
cursor | string | Opaque cursor from a previous response's meta.cursor. |
Request
curl -s "https://api.soulbound.tools/v1/abilities"
Response
{
"data": [
{
"id": "ability_aggro_reset",
"name": "Fade",
"description": "Drops all mob aggro and provides a temporary speed boost",
"category": "dexterity",
"icon": "teleport_icon"
}
],
"meta": {
"total": 39,
"count": 1,
"cursor": "eyJvIjoxfQ"
}
}
Responses
| Status | Meaning |
|---|---|
200 | A page of results. |
400 | Malformed limit, cursor or filter value. |
429 | Rate limited. |
GET/v1/abilities/{id}#
Fetch one ability
Request
curl -s "https://api.soulbound.tools/v1/abilities/ability_aggro_reset"
Response
{
"data": {
"id": "ability_aggro_reset",
"name": "Fade",
"description": "Drops all mob aggro and provides a temporary speed boost",
"category": "dexterity",
"icon": "teleport_icon"
}
}
Responses
| Status | Meaning |
|---|---|
200 | The resource. |
404 | No such resource. |
429 | Rate limited. |
GET/v1/relics#
List relics
Query parameters
| Parameter | Type | Meaning |
|---|---|---|
rarity | string | Rarity, either numeric (4) or by name (legendary). |
q | string | Case-insensitive substring match on name. |
limit | integer | Page size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200. |
cursor | string | Opaque cursor from a previous response's meta.cursor. |
Request
curl -s "https://api.soulbound.tools/v1/relics?rarity=legendary"
Response
{
"data": [
{
"id": "dc_relic_blackhole_bomb_heat_death_level1",
"name": "Heat Death",
"description": "Blackhole Bomb - On collapse, enemies take cold damage, are frozen for 1s, and chilled for 3s.",
"label": "Heat Death",
"min": 1,
"max": 1,
"rarity": 0,
"rarityName": "common",
"icon": "ability_bomb_blackhole_icon",
"unique": false
}
],
"meta": {
"total": 692,
"count": 1,
"cursor": "eyJvIjoxfQ"
}
}
Responses
| Status | Meaning |
|---|---|
200 | A page of results. |
400 | Malformed limit, cursor or filter value. |
429 | Rate limited. |
GET/v1/relics/{id}#
Fetch one relic
Request
curl -s "https://api.soulbound.tools/v1/relics/dc_relic_blackhole_bomb_heat_death_level1"
Response
{
"data": {
"id": "dc_relic_blackhole_bomb_heat_death_level1",
"name": "Heat Death",
"description": "Blackhole Bomb - On collapse, enemies take cold damage, are frozen for 1s, and chilled for 3s.",
"label": "Heat Death",
"min": 1,
"max": 1,
"rarity": 0,
"rarityName": "common",
"icon": "ability_bomb_blackhole_icon",
"unique": false
}
}
Responses
| Status | Meaning |
|---|---|
200 | The resource. |
404 | No such resource. |
429 | Rate limited. |
GET/v1/followers#
List followers
Query parameters
| Parameter | Type | Meaning |
|---|---|---|
rarity | string | Rarity, either numeric (4) or by name (legendary). |
q | string | Case-insensitive substring match on name. |
limit | integer | Page size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200. |
cursor | string | Opaque cursor from a previous response's meta.cursor. |
Request
curl -s "https://api.soulbound.tools/v1/followers?rarity=legendary"
Response
{
"data": [
{
"id": "pet_berryPatisserie",
"name": "Berry Cupcake",
"rarity": 2,
"rarityName": "rare",
"icon": "pet_berryPatisserie_icon"
}
],
"meta": {
"total": 54,
"count": 1,
"cursor": "eyJvIjoxfQ"
}
}
Responses
| Status | Meaning |
|---|---|
200 | A page of results. |
400 | Malformed limit, cursor or filter value. |
429 | Rate limited. |
GET/v1/followers/{id}#
Fetch one follower
Request
curl -s "https://api.soulbound.tools/v1/followers/pet_berryPatisserie"
Response
{
"data": {
"id": "pet_berryPatisserie",
"name": "Berry Cupcake",
"rarity": 2,
"rarityName": "rare",
"icon": "pet_berryPatisserie_icon"
}
}
Responses
| Status | Meaning |
|---|---|
200 | The resource. |
404 | No such resource. |
429 | Rate limited. |
GET/v1/stats#
The public stat vocabulary Every stat key an item may carry. Small and unpaginated.
Request
curl -s "https://api.soulbound.tools/v1/stats"
Response
{
"data": [
{
"key": "anim_speed",
"name": "Anim Speed",
"short": "Anim Speed",
"description": "Speed of the spawn animation (affects growth rate)."
},
{
"key": "aoe_radius",
"name": "Aoe Radius",
"short": "Aoe Radius",
"description": "radius of AOE"
}
],
"meta": {
"total": 58,
"count": 58
}
}
Responses
| Status | Meaning |
|---|---|
200 | A page of results. |
429 | Rate limited. |
GET/v1/meta#
Dataset version and counts
Request
curl -s "https://api.soulbound.tools/v1/meta"
Response
{
"data": {
"version": "2026-08-07.1",
"published": "2026-08-07T17:52:21Z",
"counts": {
"items": 1452,
"abilities": 39,
"relics": 692,
"followers": 54,
"stats": 58
},
"license": "Community and fan use under the terms at https://soulbound.game/legal-portal/",
"terms": "https://soulbound.game/legal-portal/",
"copyright": "© Webb Technology Limited, trading as SpiderWare. Soulbound: Online™.",
"docs": "https://api.soulbound.tools/docs"
}
}
Responses
| Status | Meaning |
|---|---|
200 | The resource. |
429 | Rate limited. |
GET/v1/img/{spriteId}.png#
Item, ability, relic and follower artwork PNG for the sprite id carried by an object's `icon` field. Served straight from static assets, so it is fast and cheap to fetch in bulk. Sprite ids are stable — cache them hard.
Request
curl -s -o icon.png "https://api.soulbound.tools/v1/img/belt_dark_2.png"
Responses
| Status | Meaning |
|---|---|
200 | PNG artwork. |
404 | No sprite with that id. |
Schemas#
The objects the routes above return. These are the same definitions
the OpenAPI document carries under
components.schemas.
Item#
An equippable or cosmetic item.
| Field | Type | Meaning | |
|---|---|---|---|
id | string | required | Stable public key. |
name | string | required | |
description | string | optional | Plain text; colour markup is stripped. |
type | string | optional | Slot family. |
slot | string | optional | |
subtype | string | optional | Absent when the item has none. |
rarity | integer | required | Numeric rarity, low to high. |
rarityName | string | required | e.g. common, rare, legendary. |
cosmetic | boolean | optional | |
icon | string | optional | Sprite id. Artwork at /v1/img/{icon}.png |
stats | array<object> | optional | Stats on this instance. Fields: stat, value, tags. |
roll | object<string, array<number>> | optional | Public min..max roll range per stat, as [min, max]. |
effects | array<object> | optional | Legendary procs. `chance` is the chance the effect fires in combat, in percent — it is not a drop rate. Fields: trigger, label, impact, cooldown, chance. |
requirements | array<object> | optional | Fields: skill, level. |
dungeons | array<string> | optional | Display names of the dungeons this item drops in. Never ids, never rates. |
Ability#
| Field | Type | Meaning | |
|---|---|---|---|
id | string | required | |
name | string | required | |
description | string | optional | |
category | string | optional | |
icon | string | optional | Sprite id. Artwork at /v1/img/{icon}.png |
Relic#
| Field | Type | Meaning | |
|---|---|---|---|
id | string | required | |
name | string | required | |
label | string | optional | |
min | number | optional | |
max | number | optional | |
unit | string | optional | |
prefix | string | optional | |
rarity | integer | optional | |
rarityName | string | optional | |
icon | string | optional | |
unique | boolean | optional |
Follower#
| Field | Type | Meaning | |
|---|---|---|---|
id | string | required | |
name | string | required | |
rarity | integer | optional | |
rarityName | string | optional | |
icon | string | optional |
Stat#
One entry in the public stat vocabulary.
| Field | Type | Meaning | |
|---|---|---|---|
key | string | required | The key used in item.stats[].stat and item.roll. |
name | string | required | |
short | string | optional | |
unit | string | optional | |
description | string | optional | |
cap | number | optional | Public balance ceiling, where one is set. |
Meta#
| Field | Type | Meaning | |
|---|---|---|---|
version | string | optional | Dataset version. Also sent as the x-dataset-version header. |
published | string | optional | RFC 3339 timestamp. |
counts | object<string, integer> | optional | |
license | string | optional | |
docs | string | optional |
Error#
| Field | Type | Meaning | |
|---|---|---|---|
error | object | required | Fields: code, message. |
CollectionMeta#
| Field | Type | Meaning | |
|---|---|---|---|
total | integer | required | Matches for this query across all pages. |
count | integer | required | Items in this page. |
cursor | string | optional | Pass as ?cursor= for the next page. Absent when exhausted. |