📄 Docs ⚙️ Dashboard 💳 Pricing 🔑 Get API Key 🏠 Main Site ↗

TCG Price Alerts

Build "tell me when Charizard drops below €600" features in any language. Two patterns: pull via REST cron, or push via WebSocket / Business+ webhooks.

When to use which

PatternLatencyTier
REST poll (cron / queue)~6 hFree+
WebSocket card:{id} stream< 5 s after scrapeStarter+
Webhook callback (HTTPS POST)< 5 sBusiness+

curl — one-shot price check

curl -H "Authorization: Bearer $CSM_KEY" \
  "https://collectorstashmarket.com/api/cards/3782/prices"

Python — cron-friendly batch alert checker

import os, time, requests, smtplib
from email.message import EmailMessage

CSM = "https://collectorstashmarket.com"
KEY = os.environ["CSM_KEY"]
HDR = {"Authorization": f"Bearer {KEY}"}

# (card_id, target_eur, email)
WATCH = [(3782, 600.0, "buyer@example.com"), (11172, 50.0, "buyer@example.com")]

def cheapest(card_id):
    rows = requests.get(f"{CSM}/api/cards/{card_id}/prices", headers=HDR, timeout=10).json().get("prices", [])
    eur  = [r["market_price"] for r in rows if r.get("currency") == "EUR" and r.get("market_price")]
    return min(eur) if eur else None

def notify(to, card_id, price, target):
    msg = EmailMessage()
    msg["From"], msg["To"] = "alerts@yourdomain.com", to
    msg["Subject"] = f"Card {card_id} dropped to €{price:.2f}"
    msg.set_content(f"Target was €{target:.2f}.\nhttps://collectorstashmarket.com/cards/{card_id}")
    with smtplib.SMTP("localhost") as s: s.send_message(msg)

for cid, target, email in WATCH:
    p = cheapest(cid)
    if p is not None and p <= target:
        notify(email, cid, p, target)
        print(f"[ALERT] {cid} {p} <= {target}")
    time.sleep(0.5)  # respect 60 req/min on Free

TypeScript / Node — push via WebSocket

import WebSocket from "ws";

const watch = new Map<number, number>([[3782, 600], [11172, 50]]); // cardId -> target EUR
const ws = new WebSocket(
  `wss://collectorstashmarket.com/api/v1/stream?api_key=${process.env.CSM_KEY}`,
);

ws.on("open", () => {
  ws.send(JSON.stringify({
    type: "subscribe",
    topics: [...watch.keys()].map((id) => `card:${id}`),
  }));
});

ws.on("message", (raw) => {
  const f = JSON.parse(raw.toString());
  if (f.type === "ping") return ws.send(JSON.stringify({ type: "pong" }));
  if (f.type !== "price_update") return;
  const { card_id, market_price, currency } = f.data;
  const target = watch.get(card_id);
  if (target && currency === "EUR" && market_price <= target) {
    console.log(`[ALERT] ${card_id} → ${market_price} EUR (target ${target})`);
    // POST to your own notify/email/SMS service here
  }
});

ws.on("close", () => setTimeout(() => process.exit(1), 5_000)); // let systemd restart

PHP — webhook receiver (Business+)

<?php
// /webhooks/csm-price.php — register this URL via the developer dashboard.
// CSM signs every webhook with X-CSM-Signature: hex(hmac_sha256(secret, body)).

$secret = getenv("CSM_WEBHOOK_SECRET");
$body   = file_get_contents("php://input");
$sig    = $_SERVER["HTTP_X_CSM_SIGNATURE"] ?? "";

if (!hash_equals(hash_hmac("sha256", $body, $secret), $sig)) {
    http_response_code(401);
    exit("bad signature");
}

$event = json_decode($body, true);
if ($event["type"] === "price.alert.triggered") {
    $card  = $event["data"]["card_id"];
    $price = $event["data"]["market_price"];
    // ship to user — your existing alert table lookup goes here
    error_log("price alert: card=$card price=$price");
}
http_response_code(200);

Node — Express webhook receiver

import crypto from "node:crypto";
import express from "express";

const app = express();
app.post("/webhooks/csm-price",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sig = req.header("x-csm-signature") ?? "";
    const expected = crypto
      .createHmac("sha256", process.env.CSM_WEBHOOK_SECRET!)
      .update(req.body).digest("hex");
    if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(401).end("bad signature");
    }
    const event = JSON.parse(req.body.toString());
    if (event.type === "price.alert.triggered") {
      // ship to user
    }
    res.status(200).end();
  });
app.listen(8080);

Related