API reference

Soulbound: Online — public game data

Public, read-only game data for Soulbound: Online. No signup, no cost, no write path.

dataset 2026-08-07.1 https://api.soulbound.tools 1452 items · 39 abilities · 692 relics · 54 followers · 58 stats

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 ranges are public. 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.

There is no MCP server for this API yet. If you are looking for one to add to Claude Code or Claude Desktop, it does not exist — point your agent at the OpenAPI document above instead, which needs no server at all. Tell us in Discord if an MCP server would actually help you; that is what would get one built.

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.

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.

CodeHTTPWhen
not_found404No route matches, or no resource with that id.
bad_request400A malformed limit, cursor or filter value.
rate_limited429Over the tier limit. Back off until retry-after.
method_not_allowed405Anything other than GET, HEAD or OPTIONS.
internal500Something 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#

TierLimitKeyed on
anonymous60 / 60sclient IP
community600 / 60skey id
images1200 / 60sclient 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.

Responses are publicly cacheable, so a 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

StatusMeaning
302Redirect 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

StatusMeaning
200HTML 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

StatusMeaning
200Plain-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

StatusMeaning
200This document.

GET/v1/items#

List items Every equippable and cosmetic item. Filters combine with AND.

Query parameters

ParameterTypeMeaning
slotstringEquipment slot, exact match, case-insensitive.
raritystringRarity, either numeric (4) or by name (legendary).
subtypestringItem subtype, exact match, case-insensitive.
cosmeticbooleantrue or false.
qstringCase-insensitive substring match on name.
limitintegerPage size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200.
cursorstringOpaque 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

StatusMeaning
200A page of results.
400Malformed limit, cursor or filter value.
429Rate 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

StatusMeaning
200The resource.
404No such resource.
429Rate limited.

GET/v1/abilities#

List abilities

Query parameters

ParameterTypeMeaning
categorystringAbility category, exact match, case-insensitive.
qstringCase-insensitive substring match on name.
limitintegerPage size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200.
cursorstringOpaque 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

StatusMeaning
200A page of results.
400Malformed limit, cursor or filter value.
429Rate 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

StatusMeaning
200The resource.
404No such resource.
429Rate limited.

GET/v1/relics#

List relics

Query parameters

ParameterTypeMeaning
raritystringRarity, either numeric (4) or by name (legendary).
qstringCase-insensitive substring match on name.
limitintegerPage size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200.
cursorstringOpaque 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

StatusMeaning
200A page of results.
400Malformed limit, cursor or filter value.
429Rate 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

StatusMeaning
200The resource.
404No such resource.
429Rate limited.

GET/v1/followers#

List followers

Query parameters

ParameterTypeMeaning
raritystringRarity, either numeric (4) or by name (legendary).
qstringCase-insensitive substring match on name.
limitintegerPage size. Default 50, maximum 200 (larger values are clamped). Default 50, max 200.
cursorstringOpaque 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

StatusMeaning
200A page of results.
400Malformed limit, cursor or filter value.
429Rate 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

StatusMeaning
200The resource.
404No such resource.
429Rate 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

StatusMeaning
200A page of results.
429Rate 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

StatusMeaning
200The resource.
429Rate 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

StatusMeaning
200PNG artwork.
404No 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.

FieldTypeMeaning
idstringrequiredStable public key.
namestringrequired
descriptionstringoptionalPlain text; colour markup is stripped.
typestringoptionalSlot family.
slotstringoptional
subtypestringoptionalAbsent when the item has none.
rarityintegerrequiredNumeric rarity, low to high.
rarityNamestringrequirede.g. common, rare, legendary.
cosmeticbooleanoptional
iconstringoptionalSprite id. Artwork at /v1/img/{icon}.png
statsarray<object>optionalStats on this instance. Fields: stat, value, tags.
rollobject<string, array<number>>optionalPublic min..max roll range per stat, as [min, max].
effectsarray<object>optionalLegendary procs. `chance` is the chance the effect fires in combat, in percent — it is not a drop rate. Fields: trigger, label, impact, cooldown, chance.
requirementsarray<object>optional Fields: skill, level.
dungeonsarray<string>optionalDisplay names of the dungeons this item drops in. Never ids, never rates.

Ability#

FieldTypeMeaning
idstringrequired
namestringrequired
descriptionstringoptional
categorystringoptional
iconstringoptionalSprite id. Artwork at /v1/img/{icon}.png

Relic#

FieldTypeMeaning
idstringrequired
namestringrequired
labelstringoptional
minnumberoptional
maxnumberoptional
unitstringoptional
prefixstringoptional
rarityintegeroptional
rarityNamestringoptional
iconstringoptional
uniquebooleanoptional

Follower#

FieldTypeMeaning
idstringrequired
namestringrequired
rarityintegeroptional
rarityNamestringoptional
iconstringoptional

Stat#

One entry in the public stat vocabulary.

FieldTypeMeaning
keystringrequiredThe key used in item.stats[].stat and item.roll.
namestringrequired
shortstringoptional
unitstringoptional
descriptionstringoptional
capnumberoptionalPublic balance ceiling, where one is set.

Meta#

FieldTypeMeaning
versionstringoptionalDataset version. Also sent as the x-dataset-version header.
publishedstringoptionalRFC 3339 timestamp.
countsobject<string, integer>optional
licensestringoptional
docsstringoptional

Error#

FieldTypeMeaning
errorobjectrequired Fields: code, message.

CollectionMeta#

FieldTypeMeaning
totalintegerrequiredMatches for this query across all pages.
countintegerrequiredItems in this page.
cursorstringoptionalPass as ?cursor= for the next page. Absent when exhausted.