Every window is laid out as [the container's own slots][the player's 36], so own_slots == size - 36. That one rule covers a single chest (27), a double (54), a copper or trapped chest, a shulker box, a hopper and a chest minecart, with nothing special-cased. type exists only to report what was opened.
Every response below was captured from a live 26.2 server.
Opening what you need
Every POST /container/* verb needs a window open. Give it the position and it opens the block itself:
| field | type | meaning |
|---|---|---|
X, Y, Z | int | the block to work at; all three or none |
face | int | which face to click, 0–5; defaults to the one facing the bot |
at | string | a merchant to open instead: the nearest entity of a type |
at_entity_id | int | a merchant by exact id |
With no position, the verb uses whatever window is already open, which is how it has always behaved. The window is left open afterwards, so a caller that opens once and acts several times still works.
It cannot find the block for you. The version tables carry item names and block classification, not block names, so “the nearest furnace” is not a question this client can answer.
POST /container/open
Parameters — give a block position or an entity type.
| name | type | required | default | meaning |
|---|---|---|---|---|
X, Y, Z | int | one form | the block to open | |
face | int | no | the face toward the bot | which face to click |
type | string | one form | the nearest entity of a type |
| field | type | meaning |
|---|---|---|
window_id | int | the server’s id for this window |
type | int | the window-type id |
title | string | the window’s title as the server sent it |
size | int | total slots, container plus the player’s 36 |
own_slots | int | how many belong to the container |
target_id, target_type | int, string | for the entity form |
A block
{
"X": 5001,
"Y": 100,
"Z": 5000,
"face": 1
}
{
"ok": true,
"pitch": 48.2397,
"size": 63,
"title": "translate container.chest",
"type": 2,
"window_id": 1,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
An entity
{
"type": "villager"
}
{
"ok": true,
"pitch": 17.223436,
"size": 39,
"target_id": 32617,
"target_type": "minecraft:villager",
"title": "insertion $6f3ac823-39fa-4e46-9261-290a36a242c2 hover_event name translate entity.minecraft.villager.farmer minecraft:villager action show_entity uuid translate entity.minecraft.villager.farmer",
"type": 19,
"window_id": 8,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
A window existing is not a window being populated: the open packet and the contents packet are separate, so this waits for both before it returns.
An entity that has only just spawned is not ready to trade for a second or so, and answers the first interact with silence. Opening repeats the interaction while it waits rather than concluding there is no UI, so a merchant summoned a moment ago opens rather than failing two times in three.
Blocks that only look like containers — a fletching table, a composter, an empty lectern — open nothing at all. A timeout on those is correct, not a bug.
GET /container
The open window’s contents.
Parameters — none.
| field | type | meaning |
|---|---|---|
open | bool | whether anything is open |
window_id, kind, type, title | which window | |
size, own_slots | int | total slots, and how many are the container’s |
items | array | slot, item, count |
truncated | bool | present when the list was cut for size |
{
"items": [
{
"count": 1,
"item": "minecraft:netherite_pickaxe",
"slot": 54
},
{
"count": 63,
"item": "minecraft:stone",
"slot": 55
},
{
"count": 7,
"item": "minecraft:snowball",
"slot": 56
},
{
"count": 15,
"item": "minecraft:oak_log",
"slot": 58
},
{
"count": 2,
"item": "minecraft:golden_apple",
"slot": 59
},
{
"count": 4,
"item": "minecraft:oak_planks",
"slot": 62
}
],
"kind": "chest",
"open": true,
"own_slots": 27,
"size": 63,
"title": "translate container.chest",
"truncated": false,
"type": 2,
"window_id": 1
}
POST /container/close
Parameters — none.
{
"ok": true,
"pitch": 48.2397,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
The server tracks which window is open, and clicks against a stale window id are accepted and ignored.
POST /container/take
Shift-clicks a container slot into the player’s inventory. Answers with the envelope alone — read GET /container back if you need to know what moved.
Parameters
| name | type | required | meaning |
|---|---|---|---|
slot | int | yes | the slot to take |
{
"slot": 0
}
{
"ok": true,
"pitch": 48.2397,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
Shift-click is all-or-nothing — it moves a whole stack. For exact counts, use /container/click one item at a time.
POST /container/put
Moves an item from the player’s rows into a container slot.
Parameters
| name | type | required | default | meaning |
|---|---|---|---|---|
item | string | yes | what to move | |
slot | int | yes | the destination slot | |
one | bool | no | false | move a single item instead of the stack |
{
"item": "minecraft:stone",
"slot": 0
}
{
"count": 63,
"item": "minecraft:stone",
"ok": true,
"pitch": 48.2397,
"slot": 0,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
Searches start from the player’s rows, never the whole window. Otherwise a search for an ingredient finds the one just placed in the crafting grid and the call picks its own work back up.
POST /container/click
The raw click, for cases the helpers do not cover.
Parameters
| name | type | required | meaning |
|---|---|---|---|
slot | int | yes | which slot |
button | int | yes | 0 left, 1 right |
mode | int | yes | 0 pickup, 1 quick-move, and the rest of the protocol’s modes |
{
"slot": 0,
"button": 0,
"mode": 1
}
{
"ok": true,
"pitch": 48.2397,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
POST /container/deposit
Moves items in bulk from the player into the container.
Parameters
| name | type | required | default | meaning |
|---|---|---|---|---|
item | string | yes | what to move | |
count | int | no | how many | |
all | bool | no | false | move everything of that item |
The response shape follows which form you used. With count, it reports moved, requested and in_container. With all, there is no requested number to compare against, so it reports stacks — how many stacks moved.
{
"item": "minecraft:stone",
"all": true
}
{
"ok": true,
"pitch": 48.2397,
"stacks": 5,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
POST /container/withdraw
The reverse.
Parameters
| name | type | required | meaning |
|---|---|---|---|
item | string | yes | what to move |
count | int | no | how many |
| field | type | meaning |
|---|---|---|
moved | int | how many actually moved |
requested | int | how many were asked for |
left_in_container | int | what remains |
{
"item": "minecraft:stone",
"count": 16
}
{
"left_in_container": 47,
"moved": 16,
"ok": true,
"pitch": 48.2397,
"requested": 16,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
moved and requested differ when the container is full or does not hold that much. That is the answer, not an error: a full chest keeps the remainder and the server says nothing about it.
POST /container/clear
Empties the player’s inventory into the open container.
Parameters
| name | type | required | default | meaning |
|---|---|---|---|---|
item | string | no | just this item | |
count | int | no | how many | |
all | bool | no | false | everything |
curl -X POST localhost:8181/container/clear -d '{"all":true}'
Returns stacks, the number of stacks moved.
POST /container/craft
Asks the server to craft from its own recipe book — one packet, and all repeats until the ingredients run out.
Parameters — give item or recipe.
| name | type | required | default | meaning |
|---|---|---|---|---|
item | string | one form | what to craft, looked up in the book | |
recipe | int | one form | a numeric recipe id from /recipes | |
all | bool | no | false | repeat until the ingredients run out |
{
"item": "minecraft:stick",
"all": false
}
{
"all": false,
"item": "minecraft:stick",
"ok": true,
"pitch": 48.2397,
"recipe": 1259,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
POST /container/grid
Lays a recipe out by slot in an open crafting table and takes the result. Preferred over /container/craft for hand-written tests: a layout is readable, a numeric recipe id is not.
Parameters
| name | type | required | default | meaning |
|---|---|---|---|---|
layout | object | yes | slot number to item name | |
repeat | int | no | 1 | craft it this many times |
Slots are "1"–"9", left to right and top to bottom.
{
"layout": {
"1": "white_wool",
"2": "white_wool",
"3": "white_wool",
"4": "white_wool",
"5": "white_wool",
"6": "white_wool",
"8": "stick"
},
"repeat": 1
}
{
"count": 1,
"item": "minecraft:white_banner",
"ok": true,
"pitch": 48.2397,
"repeat": 1,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
POST /container/button
Presses a numbered button: a stonecutter or loom selecting a recipe, an enchanting table picking a level.
Parameters
| name | type | required | meaning |
|---|---|---|---|
button | int | yes | which button |
curl -X POST localhost:8181/container/button -d '{"button":0}'
GET /trades
The open merchant’s offers, decoded.
Parameters — none.
| field | type | meaning |
|---|---|---|
open | bool | whether a window is open at all |
kind | int | the window-type id |
count | int | how many offers |
trades | array | the offers |
index | int | position in the list, which /container/trade takes |
input, input_count | string, int | the first cost |
input2 | string | the second cost, when there is one |
output, count | string, int | what it produces |
uses, max_uses | int | how spent the offer is |
disabled | bool | the server’s own “not right now” |
available | bool | !disabled && uses < max_uses |
xp | int | villager experience granted |
{
"count": 1,
"trades": [
{
"available": true,
"count": 6,
"disabled": false,
"index": 0,
"input": "minecraft:emerald",
"input_count": 1,
"max_uses": 12,
"output": "minecraft:bread",
"uses": 0,
"xp": 1
}
]
}
Spent offers are listed, not filtered away, because a test for lockout needs to see them.
An empty answer used to mean three different things — no window, a window that is not a merchant, or a merchant with nothing to sell — and the caller had to poll to find out which. Each now names itself:
| situation | answer |
|---|---|
| nothing open | 409, saying to open a merchant first |
| a merchant with no offers | 200 with count: 0 |
| a merchant that never opens | 409 from /container/open, naming what a villager with no trades means |
| an index nobody offers | 409 listing the offers there are |
A villager summoned with a profession but no Offers never generates any, so that last one will not come true by waiting — which the error says, because waiting for it is what callers were doing.
POST /container/trade
Parameters — give item or index.
| name | type | required | default | meaning |
|---|---|---|---|---|
item | string | one form | select by what the trade produces | |
index | int | one form | select by position in the offer list | |
times | int | no | 1 | how many trades to attempt |
raw | bool | no | false | select the offer and stop, without taking the result |
| field | type | meaning |
|---|---|---|
traded | int | trades that completed, measured from the stock gained |
requested | int | how many were asked for |
item, count | string, int | what the offer produces |
{
"item": "bread",
"times": 2
}
{
"item": "bread",
"ok": true,
"pitch": 17.223436,
"requested": 2,
"traded": 12,
"x": 5000.5,
"y": 100,
"yaw": -90,
"z": 5000.5
}
Selecting by output survives a villager whose offers are in a different order, which selecting by index does not.
traded may exceed requested. Taking a merchant result is a shift-click, and vanilla batches that into every use the villager has left and the player can afford, so one request against a fully-stocked villager can complete twelve trades. times is a floor, not a cap.
A trade is not counted by the server until the result is taken. Selecting an offer makes the result appear, which reads as success and is not: no statistic moves and no trade event fires. This endpoint always takes the result, except under raw.
Trading a locked-out offer is refused before the packet goes out, naming the reason. A villager that has run out accepts the trade and silently does nothing, so the alternative symptom is an unexplained timeout.