REST API v1 · Developers · API reference

Shop API reference

All 10 Shop endpoints of the BotGG REST API v1: scopes, parameters, fields, errors and ready-to-run curl, JavaScript and Python examples.

Developer docs
On this page

Endpoints

Paths are relative to https://console.botgg.app/api/v1. Send your API key as Authorization: Bearer botgg_…; IDs are strings.

Shop endpoints
EndpointScopeDescription
GET/guilds/{guildId}/shop/storesshop:readList stores
GET/guilds/{guildId}/shop/itemsshop:readList items
GET/guilds/{guildId}/shop/items/{itemId}shop:readGet an item
PATCH/guilds/{guildId}/shop/items/{itemId}shop:writeRestock or turn an item on/off
GET/guilds/{guildId}/shop/members/{userId}/inventoryshop:readA member’s inventory
GET/guilds/{guildId}/shop/purchasesshop:readList purchases
GET/guilds/{guildId}/shop/purchases/{purchaseId}shop:readGet a purchase
POST/guilds/{guildId}/shop/members/{userId}/purchasesshop:writeBuy an item for a member
POST/guilds/{guildId}/shop/members/{userId}/grantsshop:writeGive an item to a member for free
POST/guilds/{guildId}/shop/purchases/{purchaseId}/refundshop:writeRefund a purchase

Besides its own errors below, every endpoint can return these common ones:

List stores

GET/guilds/{guildId}/shop/stores

Scope: shop:read

The built-in Main store (id: null, items without a store) first, then the stores in their order.

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)

Response

Response fields · 200
FieldTypeRequiredDescription
itemsarray of objectYes
items[].idinteger | nullYesnull = the built-in Main store
items[].mainbooleanYes
items[].namestring | nullYesnull for the Main store
items[].emojistring | nullYes
items[].descriptionstring | nullYes
items[].colorstring | nullYes
items[].enabledbooleanYes
items[].channelIdsarray of stringYes
items[].itemCountintegerYes
Response · 200
{
  "items": [
    {
      "id": 98765,
      "main": true,
      "name": "Example",
      "emoji": "🪙",
      "description": "example",
      "color": "#18d7ff",
      "enabled": true,
      "channelIds": [
        "234567890123456789"
      ],
      "itemCount": 10
    }
  ]
}

Examples

curl
curl -s "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/stores" \
  -H "Authorization: Bearer $BOTGG_API_KEY"
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/stores', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os
import requests

res = requests.get(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/stores",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

List items

GET/guilds/{guildId}/shop/items

Scope: shop:read

In sort order. Only enabled items unless includeDisabled=true; hidden items are included (hidden: true).

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
storeIdquery"main"No
categoryquerystringNo1–32 characters
includeDisabledquerystringNoOne of 1, 0, true, false

Response

Response fields · 200
FieldTypeRequiredDescription
itemsarray of objectYes
items[].idintegerYes
items[].storeIdinteger | nullYesnull = Main store
items[].namestringYes
items[].descriptionstring | nullYes
items[].emojistring | nullYes
items[].priceintegerYes
items[].typestringYesOne of role, color, item
items[].categorystring | nullYes
items[].roleIdstring | nullYes
items[].durationSecintegerYesTimed role length; 0 = permanent
items[].consumablebooleanYes
items[].fulfillmentbooleanYesStaff deliver it (fulfilment request)
items[].stockinteger | nullYesnull = unlimited
items[].perUserLimitinteger | nullYes
items[].requiredRoleIdsarray of stringYes
items[].minLevelintegerYes
items[].enabledbooleanYes
items[].hiddenbooleanYes
items[].sortOrderintegerYes
currencyobjectYes
currency.namestringYes
currency.pluralstringYes
currency.emojistring | nullYes
Response · 200
{
  "items": [
    {
      "id": 98765,
      "storeId": 98765,
      "name": "Example",
      "description": "example",
      "emoji": "🪙",
      "price": 250,
      "type": "role",
      "category": "example",
      "roleId": "234567890123456789",
      "durationSec": 10,
      "consumable": true,
      "fulfillment": true,
      "stock": 10,
      "perUserLimit": 10,
      "requiredRoleIds": [
        "234567890123456789"
      ],
      "minLevel": 10,
      "enabled": true,
      "hidden": true,
      "sortOrder": 10
    }
  ],
  "currency": {
    "name": "Example",
    "plural": "example",
    "emoji": "🪙"
  }
}

Examples

curl
curl -s "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items" \
  -H "Authorization: Bearer $BOTGG_API_KEY"
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os
import requests

res = requests.get(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

Get an item

GET/guilds/{guildId}/shop/items/{itemId}

Scope: shop:read

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
itemIdpathstringYes

Response

Response fields · 200
FieldTypeRequiredDescription
idintegerYes
storeIdinteger | nullYesnull = Main store
namestringYes
descriptionstring | nullYes
emojistring | nullYes
priceintegerYes
typestringYesOne of role, color, item
categorystring | nullYes
roleIdstring | nullYes
durationSecintegerYesTimed role length; 0 = permanent
consumablebooleanYes
fulfillmentbooleanYesStaff deliver it (fulfilment request)
stockinteger | nullYesnull = unlimited
perUserLimitinteger | nullYes
requiredRoleIdsarray of stringYes
minLevelintegerYes
enabledbooleanYes
hiddenbooleanYes
sortOrderintegerYes
Response · 200
{
  "id": 98765,
  "storeId": 98765,
  "name": "Example",
  "description": "example",
  "emoji": "🪙",
  "price": 250,
  "type": "role",
  "category": "example",
  "roleId": "234567890123456789",
  "durationSec": 10,
  "consumable": true,
  "fulfillment": true,
  "stock": 10,
  "perUserLimit": 10,
  "requiredRoleIds": [
    "234567890123456789"
  ],
  "minLevel": 10,
  "enabled": true,
  "hidden": true,
  "sortOrder": 10
}

Examples

curl
curl -s "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items/234567890123456789" \
  -H "Authorization: Bearer $BOTGG_API_KEY"
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items/234567890123456789', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os
import requests

res = requests.get(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items/234567890123456789",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

Restock or turn an item on/off

PATCH/guilds/{guildId}/shop/items/{itemId}

Scope: shop:writeSensitive

Like /shopadmin restock and toggle. Send stock (null = unlimited) and/or enabled.

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
itemIdpathstringYes

Request body

Request body
FieldTypeRequiredDescription
enabledbooleanNoOff = not listed, not buyable
stockinteger | nullNoNew stock; null = unlimited
0–1,000,000

Response

Response fields · 200
FieldTypeRequiredDescription
idintegerYes
storeIdinteger | nullYesnull = Main store
namestringYes
descriptionstring | nullYes
emojistring | nullYes
priceintegerYes
typestringYesOne of role, color, item
categorystring | nullYes
roleIdstring | nullYes
durationSecintegerYesTimed role length; 0 = permanent
consumablebooleanYes
fulfillmentbooleanYesStaff deliver it (fulfilment request)
stockinteger | nullYesnull = unlimited
perUserLimitinteger | nullYes
requiredRoleIdsarray of stringYes
minLevelintegerYes
enabledbooleanYes
hiddenbooleanYes
sortOrderintegerYes
Response · 200
{
  "id": 98765,
  "storeId": 98765,
  "name": "Example",
  "description": "example",
  "emoji": "🪙",
  "price": 250,
  "type": "role",
  "category": "example",
  "roleId": "234567890123456789",
  "durationSec": 10,
  "consumable": true,
  "fulfillment": true,
  "stock": 10,
  "perUserLimit": 10,
  "requiredRoleIds": [
    "234567890123456789"
  ],
  "minLevel": 10,
  "enabled": true,
  "hidden": true,
  "sortOrder": 10
}

Examples

curl
curl -s -X PATCH "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items/234567890123456789" \
  -H "Authorization: Bearer $BOTGG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"enabled":true,"stock":10}'
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items/234567890123456789', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    enabled: true,
    stock: 10,
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os
import requests

res = requests.patch(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/items/234567890123456789",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
    },
    json={
        "enabled": True,
        "stock": 10,
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

A member’s inventory

GET/guilds/{guildId}/shop/members/{userId}/inventory

Scope: shop:read

Non-refunded purchases (owned and used), newest first; roleActive = a granted role that hasn’t expired or been removed.

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
userIdpathstringYesThe member’s Discord user ID · Discord ID (string)

Response

Response fields · 200
FieldTypeRequiredDescription
itemsarray of objectYes
items[].idintegerYes
items[].itemIdinteger | nullYesnull when the item was deleted (the snapshot stays)
items[].storeIdinteger | nullYes
items[].storeNamestring | nullYes
items[].itemNamestringYes
items[].itemEmojistring | nullYes
items[].itemTypestringYes
items[].userIdstringYes
items[].priceintegerYes0 for free grants
items[].statusstringYesOne of owned, used, refunded
items[].roleIdstring | nullYes
items[].expiresAtstring | nullYes
items[].roleRemovedAtstring | nullYes
items[].fulfillmentstringYesOne of none, pending, done
items[].bankTransactionIdinteger | nullYes
items[].refundTransactionIdinteger | nullYes
items[].usedAtstring | nullYes
items[].refundedAtstring | nullYes
items[].createdAtstringYes
items[].roleActivebooleanYes
Response · 200
{
  "items": [
    {
      "id": 98765,
      "itemId": 98765,
      "storeId": 98765,
      "storeName": "example",
      "itemName": "example",
      "itemEmoji": "🪙",
      "itemType": "example",
      "userId": "876543210987654321",
      "price": 250,
      "status": "owned",
      "roleId": "234567890123456789",
      "expiresAt": "2026-10-07T12:00:00.000Z",
      "roleRemovedAt": "2026-10-07T12:00:00.000Z",
      "fulfillment": "none",
      "bankTransactionId": 98765,
      "refundTransactionId": 98765,
      "usedAt": "2026-10-07T12:00:00.000Z",
      "refundedAt": "2026-10-07T12:00:00.000Z",
      "createdAt": "2026-10-07T12:00:00.000Z",
      "roleActive": true
    }
  ]
}

Examples

curl
curl -s "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/inventory" \
  -H "Authorization: Bearer $BOTGG_API_KEY"
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/inventory', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os
import requests

res = requests.get(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/inventory",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

List purchases

GET/guilds/{guildId}/shop/purchases

Scope: shop:read

The purchase ledger, newest first. Pass before=<nextCursor> for the next page.

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
limitqueryintegerNo1–100 · Default 25
beforequeryintegerNoat least 1
userIdquerystringNoDiscord ID (string)
itemIdqueryintegerNoat least 1
storeIdquery"main"No
statusquerystringNoOne of owned, used, refunded

Response

Response fields · 200
FieldTypeRequiredDescription
itemsarray of objectYes
items[].idintegerYes
items[].itemIdinteger | nullYesnull when the item was deleted (the snapshot stays)
items[].storeIdinteger | nullYes
items[].storeNamestring | nullYes
items[].itemNamestringYes
items[].itemEmojistring | nullYes
items[].itemTypestringYes
items[].userIdstringYes
items[].priceintegerYes0 for free grants
items[].statusstringYesOne of owned, used, refunded
items[].roleIdstring | nullYes
items[].expiresAtstring | nullYes
items[].roleRemovedAtstring | nullYes
items[].fulfillmentstringYesOne of none, pending, done
items[].bankTransactionIdinteger | nullYes
items[].refundTransactionIdinteger | nullYes
items[].usedAtstring | nullYes
items[].refundedAtstring | nullYes
items[].createdAtstringYes
nextCursorstring | nullYes
Response · 200
{
  "items": [
    {
      "id": 98765,
      "itemId": 98765,
      "storeId": 98765,
      "storeName": "example",
      "itemName": "example",
      "itemEmoji": "🪙",
      "itemType": "example",
      "userId": "876543210987654321",
      "price": 250,
      "status": "owned",
      "roleId": "234567890123456789",
      "expiresAt": "2026-10-07T12:00:00.000Z",
      "roleRemovedAt": "2026-10-07T12:00:00.000Z",
      "fulfillment": "none",
      "bankTransactionId": 98765,
      "refundTransactionId": 98765,
      "usedAt": "2026-10-07T12:00:00.000Z",
      "refundedAt": "2026-10-07T12:00:00.000Z",
      "createdAt": "2026-10-07T12:00:00.000Z"
    }
  ],
  "nextCursor": "98700"
}

Examples

curl
curl -s "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases?limit=25" \
  -H "Authorization: Bearer $BOTGG_API_KEY"
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases?limit=25', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os
import requests

res = requests.get(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
    },
    params={
        "limit": 25,
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

Get a purchase

GET/guilds/{guildId}/shop/purchases/{purchaseId}

Scope: shop:read

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
purchaseIdpathstringYes

Response

Response fields · 200
FieldTypeRequiredDescription
idintegerYes
itemIdinteger | nullYesnull when the item was deleted (the snapshot stays)
storeIdinteger | nullYes
storeNamestring | nullYes
itemNamestringYes
itemEmojistring | nullYes
itemTypestringYes
userIdstringYes
priceintegerYes0 for free grants
statusstringYesOne of owned, used, refunded
roleIdstring | nullYes
expiresAtstring | nullYes
roleRemovedAtstring | nullYes
fulfillmentstringYesOne of none, pending, done
bankTransactionIdinteger | nullYes
refundTransactionIdinteger | nullYes
usedAtstring | nullYes
refundedAtstring | nullYes
createdAtstringYes
Response · 200
{
  "id": 98765,
  "itemId": 98765,
  "storeId": 98765,
  "storeName": "example",
  "itemName": "example",
  "itemEmoji": "🪙",
  "itemType": "example",
  "userId": "876543210987654321",
  "price": 250,
  "status": "owned",
  "roleId": "234567890123456789",
  "expiresAt": "2026-10-07T12:00:00.000Z",
  "roleRemovedAt": "2026-10-07T12:00:00.000Z",
  "fulfillment": "none",
  "bankTransactionId": 98765,
  "refundTransactionId": 98765,
  "usedAt": "2026-10-07T12:00:00.000Z",
  "refundedAt": "2026-10-07T12:00:00.000Z",
  "createdAt": "2026-10-07T12:00:00.000Z"
}

Examples

curl
curl -s "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases/234567890123456789" \
  -H "Authorization: Bearer $BOTGG_API_KEY"
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases/234567890123456789', {
  method: 'GET',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
  },
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os
import requests

res = requests.get(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases/234567890123456789",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

Buy an item for a member

POST/guilds/{guildId}/shop/members/{userId}/purchases

Scope: shop:writeSensitiveIdempotency-Key

Same rules as /shop: the price is charged from the member’s Bank balance in one transaction with the stock and limits; requirements (roles, level) apply. A role item’s role is given right away (if Discord refuses, nothing is charged). Fulfilment requests and the Shop log are posted like a /shop purchase.

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
userIdpathstringYesThe member’s Discord user ID · Discord ID (string)
Idempotency-KeyheaderstringNoOptional. Send a unique value (a UUID) and reuse it when you retry: the change happens only once. How it works

Request body

Request body
FieldTypeRequiredDescription
itemIdintegerYesat least 1
expectedPriceintegerNoRefused with SHOP_ITEM_UNAVAILABLE (price_changed) when the price differs
0–1,000,000,000

Response

Response fields · 201
FieldTypeRequiredDescription
purchaseobjectYes
purchase.idintegerYes
purchase.itemIdinteger | nullYesnull when the item was deleted (the snapshot stays)
purchase.storeIdinteger | nullYes
purchase.storeNamestring | nullYes
purchase.itemNamestringYes
purchase.itemEmojistring | nullYes
purchase.itemTypestringYes
purchase.userIdstringYes
purchase.priceintegerYes0 for free grants
purchase.statusstringYesOne of owned, used, refunded
purchase.roleIdstring | nullYes
purchase.expiresAtstring | nullYes
purchase.roleRemovedAtstring | nullYes
purchase.fulfillmentstringYesOne of none, pending, done
purchase.bankTransactionIdinteger | nullYes
purchase.refundTransactionIdinteger | nullYes
purchase.usedAtstring | nullYes
purchase.refundedAtstring | nullYes
purchase.createdAtstringYes
balanceinteger | nullYesThe member’s balance afterwards (null when unchanged)
Response · 201
{
  "purchase": {
    "id": 98765,
    "itemId": 98765,
    "storeId": 98765,
    "storeName": "example",
    "itemName": "example",
    "itemEmoji": "🪙",
    "itemType": "example",
    "userId": "876543210987654321",
    "price": 250,
    "status": "owned",
    "roleId": "234567890123456789",
    "expiresAt": "2026-10-07T12:00:00.000Z",
    "roleRemovedAt": "2026-10-07T12:00:00.000Z",
    "fulfillment": "none",
    "bankTransactionId": 98765,
    "refundTransactionId": 98765,
    "usedAt": "2026-10-07T12:00:00.000Z",
    "refundedAt": "2026-10-07T12:00:00.000Z",
    "createdAt": "2026-10-07T12:00:00.000Z"
  },
  "balance": 1350
}

Examples

curl
curl -s -X POST "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/purchases" \
  -H "Authorization: Bearer $BOTGG_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"itemId":98765,"expectedPrice":10}'
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/purchases', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(), // reuse it when you retry this request
  },
  body: JSON.stringify({
    itemId: 98765,
    expectedPrice: 10,
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os, uuid
import requests

res = requests.post(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/purchases",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),  # reuse it when you retry this request
    },
    json={
        "itemId": 98765,
        "expectedPrice": 10,
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

Give an item to a member for free

POST/guilds/{guildId}/shop/members/{userId}/grants

Scope: shop:writeSensitiveIdempotency-Key

A staff grant: recorded as a purchase with price 0, no Bank change. Ignores the item’s on/off switch, a closed store, requirements and the per-member limit; stock still applies. A role item’s role is given right away.

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
userIdpathstringYesThe member’s Discord user ID · Discord ID (string)
Idempotency-KeyheaderstringNoOptional. Send a unique value (a UUID) and reuse it when you retry: the change happens only once. How it works

Request body

Request body
FieldTypeRequiredDescription
itemIdintegerYesat least 1
reasonstring | nullNoShown in the audit log
up to 200 characters

Response

Response fields · 201
FieldTypeRequiredDescription
purchaseobjectYes
purchase.idintegerYes
purchase.itemIdinteger | nullYesnull when the item was deleted (the snapshot stays)
purchase.storeIdinteger | nullYes
purchase.storeNamestring | nullYes
purchase.itemNamestringYes
purchase.itemEmojistring | nullYes
purchase.itemTypestringYes
purchase.userIdstringYes
purchase.priceintegerYes0 for free grants
purchase.statusstringYesOne of owned, used, refunded
purchase.roleIdstring | nullYes
purchase.expiresAtstring | nullYes
purchase.roleRemovedAtstring | nullYes
purchase.fulfillmentstringYesOne of none, pending, done
purchase.bankTransactionIdinteger | nullYes
purchase.refundTransactionIdinteger | nullYes
purchase.usedAtstring | nullYes
purchase.refundedAtstring | nullYes
purchase.createdAtstringYes
balanceinteger | nullYesThe member’s balance afterwards (null when unchanged)
Response · 201
{
  "purchase": {
    "id": 98765,
    "itemId": 98765,
    "storeId": 98765,
    "storeName": "example",
    "itemName": "example",
    "itemEmoji": "🪙",
    "itemType": "example",
    "userId": "876543210987654321",
    "price": 250,
    "status": "owned",
    "roleId": "234567890123456789",
    "expiresAt": "2026-10-07T12:00:00.000Z",
    "roleRemovedAt": "2026-10-07T12:00:00.000Z",
    "fulfillment": "none",
    "bankTransactionId": 98765,
    "refundTransactionId": 98765,
    "usedAt": "2026-10-07T12:00:00.000Z",
    "refundedAt": "2026-10-07T12:00:00.000Z",
    "createdAt": "2026-10-07T12:00:00.000Z"
  },
  "balance": 1350
}

Examples

curl
curl -s -X POST "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/grants" \
  -H "Authorization: Bearer $BOTGG_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"itemId":98765,"reason":"Tournament prize"}'
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/grants', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(), // reuse it when you retry this request
  },
  body: JSON.stringify({
    itemId: 98765,
    reason: 'Tournament prize',
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os, uuid
import requests

res = requests.post(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/members/876543210987654321/grants",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),  # reuse it when you retry this request
    },
    json={
        "itemId": 98765,
        "reason": "Tournament prize",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

Refund a purchase

POST/guilds/{guildId}/shop/purchases/{purchaseId}/refund

Scope: shop:writeSensitiveIdempotency-Key

Like Refund in the console: the price goes back to the member’s balance (one transaction), the purchase becomes refunded, optionally restocked; a granted role is removed by the bot within ~30 s.

Needs this feature enabled on the server: Shop

Parameters
NameInTypeRequiredDescription
guildIdpathstringYesYour server’s ID · Discord ID (string)
purchaseIdpathstringYes
Idempotency-KeyheaderstringNoOptional. Send a unique value (a UUID) and reuse it when you retry: the change happens only once. How it works

Request body

Request body
FieldTypeRequiredDescription
restockbooleanNoPut the unit back in stock (default: the Shop’s “restock on refund” setting)
forcebooleanNoAlso refund an item that was already used
reasonstring | nullNoShown in the audit log
up to 200 characters

Response

Response fields · 200
FieldTypeRequiredDescription
purchaseobjectYes
purchase.idintegerYes
purchase.itemIdinteger | nullYesnull when the item was deleted (the snapshot stays)
purchase.storeIdinteger | nullYes
purchase.storeNamestring | nullYes
purchase.itemNamestringYes
purchase.itemEmojistring | nullYes
purchase.itemTypestringYes
purchase.userIdstringYes
purchase.priceintegerYes0 for free grants
purchase.statusstringYesOne of owned, used, refunded
purchase.roleIdstring | nullYes
purchase.expiresAtstring | nullYes
purchase.roleRemovedAtstring | nullYes
purchase.fulfillmentstringYesOne of none, pending, done
purchase.bankTransactionIdinteger | nullYes
purchase.refundTransactionIdinteger | nullYes
purchase.usedAtstring | nullYes
purchase.refundedAtstring | nullYes
purchase.createdAtstringYes
balanceinteger | nullYesThe member’s balance afterwards (null when unchanged)
Response · 200
{
  "purchase": {
    "id": 98765,
    "itemId": 98765,
    "storeId": 98765,
    "storeName": "example",
    "itemName": "example",
    "itemEmoji": "🪙",
    "itemType": "example",
    "userId": "876543210987654321",
    "price": 250,
    "status": "owned",
    "roleId": "234567890123456789",
    "expiresAt": "2026-10-07T12:00:00.000Z",
    "roleRemovedAt": "2026-10-07T12:00:00.000Z",
    "fulfillment": "none",
    "bankTransactionId": 98765,
    "refundTransactionId": 98765,
    "usedAt": "2026-10-07T12:00:00.000Z",
    "refundedAt": "2026-10-07T12:00:00.000Z",
    "createdAt": "2026-10-07T12:00:00.000Z"
  },
  "balance": 1350
}

Examples

curl
curl -s -X POST "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases/234567890123456789/refund" \
  -H "Authorization: Bearer $BOTGG_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"restock":true,"force":true,"reason":"Tournament prize"}'
JavaScript (fetch)
// Node.js 18+ (built-in fetch) — on your server, never in a browser
const res = await fetch('https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases/234567890123456789/refund', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.BOTGG_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': crypto.randomUUID(), // reuse it when you retry this request
  },
  body: JSON.stringify({
    restock: true,
    force: true,
    reason: 'Tournament prize',
  }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error}: ${data.message}`);
console.log(data);
Python (requests)
import os, uuid
import requests

res = requests.post(
    "https://console.botgg.app/api/v1/guilds/123456789012345678/shop/purchases/234567890123456789/refund",
    headers={
        "Authorization": f"Bearer {os.environ['BOTGG_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),  # reuse it when you retry this request
    },
    json={
        "restock": True,
        "force": True,
        "reason": "Tournament prize",
    },
    timeout=10,
)
data = res.json()
if not res.ok:
    raise RuntimeError(f"{res.status_code} {data['error']}: {data['message']}")
print(data)

Errors

Ready to level up your server?

Add BotGG for free, then switch features on in the web console.