Create a price watch
Fire price.threshold.crossed when one quote of one card crosses a price.
A watch fires on the move that crosses the threshold, not on every day the price sits beyond it: a price already below a below threshold when the watch is created fires only after it goes back above and falls through again.
A localized printing quoted as its English twin is watched on the English printing, because that is where its quotes are. The response's cardId is the English id and requestedCardId is the one you sent.
Plans cap how many watches an account holds: 500 on Starter, 25,000 on Growth, 100,000 on Scale.
Request body
JSON, with Content-Type: application/json. A body that is not valid JSON is a 400 reading “Send a JSON body.”
| Parameter | Type | Description |
|---|---|---|
| cardIdrequired | string | The card to watch. |
| sourcerequired | string | The marketplace. The threshold is in its currency: EUR on Cardmarket, USD elsewhere. One of: |
| directionrequired | string | Fire when the price rises above, or falls below, the threshold. One of: |
| threshold | number | The price in major units (12.5). Send this or thresholdCents. |
| thresholdCents | integer | The price in minor units (1250). Wins over threshold. |
| finish | string | Which printing's quote. Without it, the finish of the card's current quote on the source and list, else normal.One of: Default: the finish the card is quoted in on |
| listType | string | buylist only on cardkingdom, the one source that publishes one.One of: Default: |
Response
201 The watch.
| Field | Type | Description |
|---|---|---|
| data | Watch (Watch reference) | A Watch with the card's game and name. |
| data.game | string | The card's game. One of: |
| data.name | string | The card's name. |
| data.requestedCardId | string | The id you sent, when the watch was placed on its English twin. Absent unless it applies. |
Examples
Real responses from the production API, recorded on 2026-09-24 by the script that also checks every field above against them. Arrays are shown in full.
Tell me when Charizard ex drops under €20
curl -X POST "https://api.tcggraph.com/v1/watches" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"cardId":"pkm_sv3pt5_6","source":"cardmarket","direction":"below","threshold":20}'const response = await fetch("https://api.tcggraph.com/v1/watches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"cardId": "pkm_sv3pt5_6",
"source": "cardmarket",
"direction": "below",
"threshold": 20
}),
});
const body = await response.json();
if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);import os
import requests
response = requests.post(
"https://api.tcggraph.com/v1/watches",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"cardId": "pkm_sv3pt5_6", "source": "cardmarket", "direction": "below", "threshold": 20},
)
body = response.json()
response.raise_for_status(){
"data": {
"id": "c245e388-33da-446a-89b1-b6afc908cf05",
"cardId": "pkm_sv3pt5_6",
"source": "cardmarket",
"finish": "foil",
"listType": "retail",
"currency": "EUR",
"direction": "below",
"thresholdCents": 2000,
"threshold": 20,
"lastFiredAt": null,
"createdAt": "2026-09-24T07:47:45.432Z",
"game": "pokemon",
"name": "Charizard ex"
}
}A German printing, watched on its English twin
curl -X POST "https://api.tcggraph.com/v1/watches" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"cardId":"pkm_sv3pt5_6_de","source":"cardmarket","direction":"above","threshold":100}'const response = await fetch("https://api.tcggraph.com/v1/watches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"cardId": "pkm_sv3pt5_6_de",
"source": "cardmarket",
"direction": "above",
"threshold": 100
}),
});
const body = await response.json();
if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);import os
import requests
response = requests.post(
"https://api.tcggraph.com/v1/watches",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"cardId": "pkm_sv3pt5_6_de", "source": "cardmarket", "direction": "above", "threshold": 100},
)
body = response.json()
response.raise_for_status(){
"data": {
"id": "37f6d8df-ac25-438c-90a8-7cfd63892f80",
"cardId": "pkm_sv3pt5_6",
"source": "cardmarket",
"finish": "foil",
"listType": "retail",
"currency": "EUR",
"direction": "above",
"thresholdCents": 10000,
"threshold": 100,
"lastFiredAt": null,
"createdAt": "2026-09-24T07:47:45.687Z",
"game": "pokemon",
"name": "Charizard ex",
"requestedCardId": "pkm_sv3pt5_6_de"
}
}Errors
Every error has the same body; switch on error.code. Any endpoint can also answer 401, 402, 403 or 429 for reasons of key, plan or rate; those are on Errors.
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | A field is missing or invalid, including a buylist watch on a source without a buylist. |
| 403 | forbidden | The account holds its plan's allowance of watches. details carries allowance and held. |
| 404 | not_found | No card has that id. |
| 409 | conflict | The same watch (card, source, finish, list, direction and threshold) exists. |
Buylist on Cardmarket
curl -X POST "https://api.tcggraph.com/v1/watches" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"cardId":"pkm_sv3pt5_6","source":"cardmarket","direction":"above","threshold":5,"listType":"buylist"}'const response = await fetch("https://api.tcggraph.com/v1/watches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"cardId": "pkm_sv3pt5_6",
"source": "cardmarket",
"direction": "above",
"threshold": 5,
"listType": "buylist"
}),
});
const body = await response.json();
if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);import os
import requests
response = requests.post(
"https://api.tcggraph.com/v1/watches",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"cardId": "pkm_sv3pt5_6", "source": "cardmarket", "direction": "above", "threshold": 5, "listType": "buylist"},
)
body = response.json()
response.raise_for_status(){
"error": {
"code": "invalid_request",
"message": "Cardmarket publishes no buylist; only cardkingdom does.",
"details": {
"field": "listType",
"allowed": [
"retail"
]
}
}
}Unknown card
curl -X POST "https://api.tcggraph.com/v1/watches" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"cardId":"pkm_nope_1","source":"cardmarket","direction":"below","threshold":5}'const response = await fetch("https://api.tcggraph.com/v1/watches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"cardId": "pkm_nope_1",
"source": "cardmarket",
"direction": "below",
"threshold": 5
}),
});
const body = await response.json();
if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);import os
import requests
response = requests.post(
"https://api.tcggraph.com/v1/watches",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"cardId": "pkm_nope_1", "source": "cardmarket", "direction": "below", "threshold": 5},
)
body = response.json()
response.raise_for_status(){
"error": {
"code": "not_found",
"message": "No card with id \"pkm_nope_1\"."
}
}Same watch twice
curl -X POST "https://api.tcggraph.com/v1/watches" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"cardId":"pkm_sv3pt5_6","source":"cardmarket","direction":"below","threshold":20}'const response = await fetch("https://api.tcggraph.com/v1/watches", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"cardId": "pkm_sv3pt5_6",
"source": "cardmarket",
"direction": "below",
"threshold": 20
}),
});
const body = await response.json();
if (!response.ok) throw new Error(`${body.error.code}: ${body.error.message}`);import os
import requests
response = requests.post(
"https://api.tcggraph.com/v1/watches",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"cardId": "pkm_sv3pt5_6", "source": "cardmarket", "direction": "below", "threshold": 20},
)
body = response.json()
response.raise_for_status(){
"error": {
"code": "conflict",
"message": "That watch already exists on this account."
}
}