Price alerts and webhooks

A price alert watches one printing's Cardmarket trend price (EUR) and POSTs a signed JSON event to your webhook endpoint when its condition is met. Alerts are checked once a day, after Cardmarket's daily price guide is imported (about 07:00 UTC).

1. Register an endpoint

Your endpoint must be a public https URL. The signing secret is returned only when you create the endpoint (or rotate its secret), so store it.

mutation { createWebhookEndpoint(url: "https://example.com/hooks/raritydb") { secret webhookEndpoint { id } } }
raritydb webhooks add https://example.com/hooks/raritydb
raritydb webhooks test <id>          # sends a signed "ping" now and shows the response

2. Create alerts

mutation($endpoint: ID!) {
  createPriceAlert(cardCode: "sv03.5-006", printingKey: "holo", condition: TREND_ABOVE, threshold: 50, webhookEndpointId: $endpoint) {
    id card { name } printing { key }
  }
}
raritydb alerts add sv03.5-006 --above 50 --webhook <id>
raritydb alerts add base1-4 --printing <key> --change 15 --webhook <id>
raritydb alerts list
Condition Threshold Fires
TREND_ABOVE EUR The day the trend rises to the threshold or above (not again while it stays there)
TREND_BELOW EUR The day the trend falls to the threshold or below
CHANGE_ABOVE_PERCENT percent Every day the trend moves up or down by at least that much from the previous price

Pass printingKey when the card has more than one printing with a Cardmarket price (see Card.printings). Printings without a Cardmarket product have no prices to watch. Each alert fires at most once a day.

Your plan allows 50 alerts and 3 endpoints (viewer { priceAlertLimit webhookEndpointLimit }).

The event

POST /hooks/raritydb
Content-Type: application/json
X-RarityDB-Event: price_alert.triggered
X-RarityDB-Delivery: 4b0c5c1e-…            (unique per delivery; use it to ignore duplicates)
X-RarityDB-Signature: t=1792742400,v1=5f2c…
{
  "event": "price_alert.triggered",
  "alert": { "id": "…", "condition": "TREND_ABOVE", "threshold": 50.0 },
  "card": { "code": "sv03.5-006", "name": "Charizard ex", "number": "006", "set": { "code": "sv03.5", "name": "151" } },
  "printing": { "key": "holo", "finish": "holo", "stamps": [], "subtype": null },
  "price": { "source": "cardmarket", "currency": "EUR", "asOf": "2026-10-23", "trend": 52.1, "previousTrend": 48.9,
             "previousAsOf": "2026-10-22", "changePercent": 6.54 }
}

Answer with any 2xx status within 10 seconds. Anything else is retried 5 times with increasing waits. After 5 failed deliveries in a row, the endpoint shows failingSince until one succeeds. viewer { webhookEndpoints { deliveries { status responseCode error } } } shows the last 20.

Verify the signature

The signature is an HMAC-SHA256 of "{t}.{raw body}" with your endpoint's secret, hex encoded, in the v1 part. Check it against the raw body, and reject old timestamps (5 minutes is a good window):

import { verifyWebhook } from "@raritydb/sdk";

if (!(await verifyWebhook(rawBody, request.headers["x-raritydb-signature"], process.env.RARITYDB_WEBHOOK_SECRET!))) {
  return new Response("bad signature", { status: 400 });
}
t, v1 = header.split(",").to_h { |part| part.split("=", 2) }.values_at("t", "v1")
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{t}.#{raw_body}")
valid = ActiveSupport::SecurityUtils.secure_compare(expected, v1.to_s) && (Time.now.to_i - t.to_i).abs < 300

If a secret leaks, rotate it (rotateWebhookSecret, raritydb webhooks rotate <id>); the old one stops working immediately.

From an agent (MCP)

The webhook_endpoints and price_alerts tools take an action (list, create, delete, and for endpoints rotate_secret and test). The general graphql_query tool is read-only.

This page as Markdown: /docs/price-alerts.md