Alerts API · MCP · Webhooks

The Pokemon TCG restock API for shops, apps and AI agents

The same real-time restock alerts our app subscribers get, as data you can build on: what just came in stock and where, its price against MSRP, and when each retailer tends to restock. Connect an AI agent over the Model Context Protocol, call it from your own code, or receive signed webhooks on your server.

Get a free key Live tier — $29.99/month

Free keys are issued instantly. No sales call, no waiting.

Launch offer: $10 off your first month of Live, applied automatically at checkout. New Live customers, ends Nov 30.

Who it’s for

Card shops and sellers

Price your shelf against retail with get_product_msrp and the markup on every listing. See which big-box retailers have been restocking and when with get_retailer_stats, so you can plan your own stock and events. Put a “restocked at retail” feed on your site or Discord to keep collectors coming back to you.

Developers and bots

Add restock alerts to your app, Discord bot or site without building a monitor. Hold a long-poll for the next matching drop, or have us push each alert to your server as a signed webhook. Alerts carry a cleaned retailer link, price, MSRP and markup where we have them, and a confidence flag.

AI agents

Add one MCP server and your assistant can answer “is anything in stock?”, wait for a specific product at MSRP, or tell whether a price is fair. Setup takes a couple of minutes in Claude Code, Cursor, VS Code or Claude Desktop.

Creators and communities

Run a restock feed for your followers on a Live key, shown at least five minutes after each alert (real-time for an audience is available by agreement).

How it works

Endpointhttps://bujusjuj.us/mcp
ProtocolMCP over Streamable HTTP (JSON-RPC 2.0). Protocol versions 2025-06-18 and 2025-03-26. Stateless: every request stands alone.
AuthAuthorization: Bearer bjs_… on every tool call. initialize, tools/list and ping also answer without a key, so you can explore the server before signing up.
Tools10 — see Tools
WebhooksOptional, HMAC-SHA256 signed — see Webhooks

Quickstart

1. Get a key

Go to bujusjujus.com/api-access, enter your email, and open the link we send you. Your developer console is there: take a free Reference key straight away, or subscribe to Live. You don't need an account first — we'll make one.

A key looks like bjs_ followed by 43 characters. It is shown once; we only store a hash, so save it somewhere safe. Lost one? Revoke it in the console and mint another.

2. Connect your agent

Claude Code

claude mcp add --transport http bujusjujus https://bujusjuj.us/mcp \
  --header "Authorization: Bearer $BUJUS_API_KEY"

Any MCP client that supports remote HTTP servers with custom headers (the exact file and key names vary by client):

{
  "mcpServers": {
    "bujusjujus": {
      "type": "http",
      "url": "https://bujusjuj.us/mcp",
      "headers": { "Authorization": "Bearer bjs_YOUR_KEY" }
    }
  }
}

For a client that only runs local (stdio) servers, bridge it with npx mcp-remote https://bujusjuj.us/mcp --header "Authorization: Bearer bjs_YOUR_KEY".

Step-by-step instructions for Claude Desktop, Claude Code, Cursor and VS Code are in the setup guide. Runnable examples (Node, Python webhooks, a Discord feed) are on GitHub.

3. Ask

"What Pokemon products are in stock right now?" · "Tell me the moment a Pitch Black Elite Trainer Box drops at MSRP." · "Is $65 a fair price for that box?"

Or call it directly

Every call is a JSON-RPC POST. Tool results come back as JSON text in result.content[0].text.

curl -s https://bujusjuj.us/mcp \
  -H "Authorization: Bearer $BUJUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"get_in_stock_now","arguments":{"minutes":30}}}'

get_in_stock_now above is a Live tool. On a free Reference key it answers tier_required — try list_recent_drops instead, or call tools/list to see exactly what your key can reach.

Other methods: initialize, tools/list (full JSON Schemas for every tool) and ping. These three are free and are never metered.

Reading the response

The rows are not a field on result. They are a JSON string inside result.content[0].text, so parse that string a second time:

{"jsonrpc":"2.0","id":1,"result":{
  "content":[{"type":"text","text":"{\"alerts\":[{\"id\":\"c108d9022699463c\", …}],\"cursor\":179019721200000}"}],
  "isError":false}}
# shell
curl -s … | jq '.result.content[0].text | fromjson | .alerts'

// JavaScript
const { result } = await res.json();
if (result.isError) throw new Error(result.content[0].text);   // "code: message"
const { alerts, cursor } = JSON.parse(result.content[0].text);
ToolThe parsed text is
get_in_stock_now{ alerts, cursor }
wait_for_drop{ alerts, cursor, timedOut }
list_recent_dropsAn array of alerts
search_tracked_products, get_product_msrpAn array of { name, retailer, msrp }
get_retailer_statsAn array of { retailer, drops }, busiest first
set_webhook, get_webhook, test_webhook, remove_webhookAn object; see Webhooks

An MCP client such as Claude, Cursor or VS Code does this for you.

The alert object

Every tool that returns alerts uses this shape:

{
  "id": "c108d9022699463c",          // stable, opaque; dedupe on this
  "detectedAt": "2026-09-23T21:00:12.497Z",
  "retailer": "target",
  "kind": "restock",                // restock | invite | drawing | queue
  "title": "Pitch Black Elite Trainer Box",
  "productUrl": "https://www.target.com/p/-/A-...",   // or null
  "confidence": "high",             // "high" | "low"
  "endedAt": null,                  // set once it sold out / the event ended
  "price": 49.99,                   // USD, or null
  "msrp": 49.99,                    // USD, or null
  "markupPct": 0                    // % over MSRP, or null
}

Polling, long-poll or webhook?

HowYou get each alertBest for
Poll get_in_stock_nowOn your next callAgents answering “what's in stock?”, dashboards
Long-poll wait_for_dropAbout a second after we detect itBots and scripts without a public URL
Webhook (set_webhook)About a second after we detect it, pushed to youServers with a public HTTPS endpoint

Popular drops can sell out in about a minute, so a poll every few minutes will miss some. Long-poll and webhooks are Live features. For past alerts, sold out or not, use list_recent_drops.

Runnable examples: a minimal client, a wait_for_drop loop, a webhook receiver that verifies signatures and recent drops.

Tools

Arguments not listed are rejected (so a typo can't silently widen your results). Retailer values: amazon, bestbuy, costco, gamestop, hottopic, macys, pokemoncenter, samsclub, target, walmart.

get_in_stock_nowLive

What just came in stock, newest first. Returns { alerts, cursor }.

ArgumentDescription
minutesLook-back window, 1–60 (default 15)
retailerOnly this retailer
queryCase-insensitive product-name substring, e.g. "Pitch Black"
maxMarkupPctSkip alerts priced more than this % over MSRP (alerts with no price or no MSRP are kept)
minConfidence"high" (default) or "low"
kind"restock" (default), "invite", "drawing", "queue" or "any"
limit1–100 (default 25)

wait_for_dropLive

A long-poll: the call stays open until a matching alert arrives or timeoutSec passes. Returns { alerts, cursor, timedOut }. A call that times out empty is not billed.

ArgumentDescription
cursorFrom your previous response. Omit to wait for the next new alert.
timeoutSec1–45 (default 20). Keep it below your client’s own request timeout; many MCP hosts cut a tool call at 20–60 s.
retailer, query, maxMarkupPct, minConfidence, kindSame filters as above

Loop on it, always passing back the returned cursor, so you never miss or repeat an alert:

let cursor;
for (;;) {
  const r = await callTool('wait_for_drop', { cursor, query: 'Pitch Black', maxMarkupPct: 10, timeoutSec: 20 });
  cursor = r.cursor;
  for (const alert of r.alerts) act(alert);   // dedupe on alert.id
}

Up to 5 waits per key can be open at once.

timeoutSec is not a delay: a matching alert comes back about a second after we detect it, whatever the timeout. It is only how long one call may stay open. If your client gives up on a call first, you lose nothing — call again with the same cursor and anything that arrived in between is returned. Running a server-side bot? Use set_webhook and we push each alert to you instead.

list_recent_dropsFree

Alert history, newest first, all kinds. retailer, sinceHours (1–2160, i.e. up to 90 days), minConfidence, kind, limit (1–200, default 50).

search_tracked_productsFree

The product catalogue we track, with MSRP. query, retailer, limit. Returns { name, retailer, msrp }. This is the tracked list, not a stock check.

get_product_msrpFree

MSRP for products matching query (required). Products without a known MSRP are left out, so an empty result means "unknown", not "free". Use specific names ("Pitch Black Elite Trainer Box") — a short query also matches packs and bundles.

get_retailer_statsFree

Drop counts per retailer over days (1–90, default 30), busiest first. Includes unverified reports, so read it as relative activity.

set_webhook · get_webhook · test_webhook · remove_webhookLive

Register, inspect, test or remove your webhook. See Webhooks below.

Webhooks

For a server-side bot that shouldn't hold a connection open. Register one HTTPS endpoint per key:

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"set_webhook",
  "arguments":{"url":"https://hooks.example.com/bujus","query":"Pitch Black","maxMarkupPct":10}}}

The response includes a signing secret (whsec_…), shown once. Calling set_webhook again replaces the URL and filters and issues a new secret. get_webhook shows status, failures and the last error (never the secret).

What we send

POST https://hooks.example.com/bujus
Content-Type: application/json
User-Agent: bujusjujus-alerts-webhook/1
X-Bujus-Signature: t=1790197212,v1=5b3f…

{"type":"alert","cursor":179019721200000,"sentAt":"2026-09-23T21:00:13.004Z","alert":{ …alert object… }}

Test your endpoint

Don't wait for a drop to find out whether your receiver works. Right after set_webhook, call test_webhook: we send one signed POST to your URL straight away and tell you what it answered.

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"test_webhook","arguments":{}}}

// result, once parsed:
{"delivered":true,"status":204,"error":null,"tookMs":184,
 "sentAt":"2026-10-28T17:30:00.000Z","url":"https://hooks.example.com/bujus","active":true}

The body is {"type":"test","sentAt":…,"note":…}, signed with your secret like a real delivery, and has no alert field, so a receiver that acts on alert won't post anything. When it fails you get delivered: false, the status or error, and a hint saying what to fix. A test isn't billed or retried, doesn't count toward the 20-failure limit, and can run 10 times an hour. It works on a disabled webhook too, so you can check a fix before re-enabling it with set_webhook.

Verify every request

The signature is HMAC-SHA256(secret, "<t>.<raw body>") as hex. Use the raw request bytes, compare in constant time, and reject a t more than 5 minutes old.

// Node.js
const crypto = require('crypto');
function verify(secret, rawBody, header) {
  const p = Object.fromEntries(header.split(',').map(kv => kv.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const want = crypto.createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest();
  const got = Buffer.from(p.v1 || '', 'hex');
  return got.length === want.length && crypto.timingSafeEqual(got, want);
}
# Python
import hmac, hashlib, time
def verify(secret: str, raw_body: bytes, header: str) -> bool:
    p = dict(kv.split("=", 1) for kv in header.split(","))
    if abs(time.time() - int(p.get("t", 0))) > 300:
        return False
    want = hmac.new(secret.encode(), p["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, p.get("v1", ""))

Limits

LimitValue
Daily quotaLive: 20,000 tool calls per UTC day. Reference: 5,000. Webhook deliveries count; empty wait_for_drop timeouts, initialize, tools/list and ping don't.
Per key120 requests per minute
Per IP600 requests per minute with a key; 20 per minute without one
Concurrent waits5 open wait_for_drop calls per key
Request size64 KB

When limited you get HTTP 429 with a Retry-After header. Back off and retry.

Errors

A request refused before any tool runs gets an HTTP error status and a JSON-RPC error object. Where there is one, error.data.error names the case.

HTTPMeaningWhat to do
400The body isn't valid JSON (-32700), or the MCP-Protocol-Version header names a version we don't support (-32600; error.data.supported lists ours)Send Content-Type: application/json and one JSON-RPC object. Use 2025-06-18 or 2025-03-26, or leave the header out.
401missing_key, invalid_key or key_revokedSend Authorization: Bearer bjs_… with exactly one space. A revoked key can't come back: create a new one in your console.
403account_inactive: the account behind the key is no longer active. Or Origin not allowed: the request came from a web page on another site.Email us about the account. Call the API from your server, not a browser; keys must stay server-side.
405A GET or DELETE on /mcpUse POST. There is no SSE stream and no session to close.
413The body is over 64 KBSend one JSON-RPC request per call.
429A rate limit, or the daily quota (error.data then has used, limit and resetsAt)Wait Retry-After seconds, then retry. See Limits.
500Something failed on our sideRetry. If it keeps happening, email us with the time.

A malformed call that gets past those checks returns HTTP 200 with a JSON-RPC error: -32601 for an unknown method, -32602 when tools/call has no name or its arguments isn't an object, -32600 for an id that isn't a string, number or null.

A tool that runs but refuses returns HTTP 200 with result.isError: true and a text of the form code: message. Refused calls are not billed.

CodeMeaning and fix
tier_requiredA Live tool called with a free Reference key. list_recent_drops, search_tracked_products, get_product_msrp and get_retailer_stats work on every key; Live adds the rest.
invalid_argumentsAn argument is unknown, the wrong type or out of range, and the message names it. Unknown arguments are refused on purpose, so a typo can't silently widen your results. tools/list has every schema.
unknown_toolNo tool by that name. Check tools/list.
too_many_waitsMore than 5 open wait_for_drop calls on one key. Run one wait loop per key.
busyWe're at long-poll capacity. Retry after a few seconds.
invalid_webhookThe webhook URL was refused (it must be public https on port 443), or it changed more than 10 times in an hour.
tool_failedThe tool failed on our side. Retry; email us if it keeps happening.

Troubleshooting

Most “it's broken” reports turn out to be one of these. Your developer console shows, for each key, the last 24 hours of calls and how many returned rows, the alerts the key was sent, which clients used it and how its webhook is doing. It's the quickest way to see what we sent you.

Every call returns no rows

curl -s https://bujusjuj.us/mcp \
  -H "Authorization: Bearer $BUJUS_API_KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_recent_drops","arguments":{"sinceHours":72}}}' \
  | jq -r '.result.content[0].text | fromjson | .[] | [.detectedAt, .retailer, .title, .price] | @tsv'

An alert has no price

price: null on a confirmed alert means the retailer didn't show us a price, not that the item is marked up. To keep only fair prices, pass maxMarkupPct (for example 0): it skips alerts priced over MSRP and keeps alerts with no price.

I missed a drop, or my alerts arrive late

The same alert comes back on every call

That's expected: get_in_stock_now keeps returning an alert while the item is in stock, so deduplicate on id. When it sells out the alert gets an endedAt and leaves get_in_stock_now. A later restock of the same product is a new alert with a new id.

wait_for_drop comes back empty, or my client cuts it off

"timedOut": true with no alerts means nothing matched within timeoutSec, and it isn't billed; call again straight away with the returned cursor. Keep timeoutSec below your client's own request timeout. If your client gives up first you lose nothing: call again with the same cursor.

My webhook gets nothing

Is it you or us?

GET https://bujusjuj.us/health answers {"ok":true,"service":"alerts-api","fresh":true}. "fresh": false means our feed is behind and new alerts will wait until it catches up. If that lasts more than a few minutes, tell us.

Still stuck? What to send us

Email [email protected] with:

We keep a record of each call your key makes and the alerts it was sent (section 10 of the API Terms), so we can tell you exactly what your key received, and when.

Security

Tiers

Launch offer: $10 off your first month of Live, applied automatically at checkout. New Live customers, ends Nov 30.
Reference — freeLive — $29.99/month
Alert dataSame rows, 5 minutes laterThe moment we see it, no holdback
Toolslist_recent_drops, search_tracked_products, get_product_msrp, get_retailer_statsThose four plus get_in_stock_now, wait_for_drop and the four webhook tools
Webhooks—Signed, with retries
Calls per day5,00020,000
IssuedInstantlyInstantly, on payment

A five-minute-old restock signal is not a substitute for a live one — Target drops have sold out in around 78 seconds — so Reference is for building and testing against real data, not for running on.

Get a key

Enter your email, open the link we send, and take a key. Live subscriptions renew monthly until cancelled; cancel any time at bujusjujus.com/account-help and the key keeps working until the end of the paid period.

The API is sold for business use. Issuing a key is automatic and implies no approval or endorsement of what you build — use of it is governed by the Alerts API Terms and the Terms of Service, and you are responsible for how you use the data, including following each retailer's terms and purchase limits.

FAQ

Is there a free tier?

Yes. Reference keys are free and issued instantly with no card. They return the same alerts five minutes later, with four tools and 5,000 calls a day. They are for evaluation and development; a product your users rely on needs a Live key.

How fast is the Live tier?

A Live key gets each alert about a second after we detect it, the same moment our own app subscribers are notified. There is no holdback.

Can I show the data to my customers or followers?

Yes, in your own products, with one rule: anything people outside your organization see must be at least five minutes past the alert’s detectedAt time. Real-time alerts for your audience need a separate agreement; email [email protected].

Which AI apps work with it?

Any MCP client that can connect to a remote HTTP server and send an Authorization header, such as Claude Code, Cursor and VS Code. Apps that only run local servers, such as Claude Desktop, connect through the mcp-remote bridge. The setup guide at bujusjujus.com/api-setup has the steps for each.

Can I use it to buy products automatically?

No. The API delivers information, not purchases. Our terms prohibit using the data for automated purchasing or to get around a retailer’s terms or purchase limits.

How long can I keep the data?

Up to 30 days, as long as you need it to run your product, and not as a historical archive. If your licence ends, delete it within 7 days. Section 4 of the Alerts API Terms has the details.

Why does get_in_stock_now return nothing?

Usually because nothing is in stock. It returns only restocks from the last 15 minutes (up to 60) that haven't sold out, and most drops happen overnight US time and sell out within minutes. Call list_recent_drops to see recent alerts, and use wait_for_drop or a webhook to catch the next one. Also check that you parse the rows from result.content[0].text. More in Troubleshooting.

Why does an alert have no price?

price is null when the retailer didn't show us a price for that alert. It means unknown, not marked up. To keep only fair prices, filter with maxMarkupPct, which keeps alerts that have no price.

Can I get higher limits, real-time redistribution or an invoice?

Yes. Email [email protected] with what you are building and we will set it up.

Changelog

Changes you might notice as a client, newest first.

Support

Email [email protected] — a real person reads it.