Create a webhook
Register an HTTPS endpoint for signed event deliveries.
The response carries secret, the key deliveries are signed with. It is returned here and never again; store it. How to verify a signature is on the webhooks page.
Management calls cost no credits and deliveries are free. They still count toward the per-minute rate limit.
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 |
|---|---|---|
| urlrequired | string | Public https URL. Private, loopback and link-local addresses, localhost, and URLs with credentials in them are refused. |
| eventsrequired | string[] | At least one event type. Duplicates are dropped. One of: |
| games | string[] | Only events about these games. One of: Default: [] (every game) |
| description | string | Your label, up to 200 characters. Default: |
Response
201 The endpoint, with its signing secret.
| Field | Type | Description |
|---|---|---|
| data | WebhookEndpoint (WebhookEndpoint reference) | A WebhookEndpoint with secret. |
| data.secret | string | whsec_ followed by the signing key. Shown once. |
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.
New sets and bans, Pokémon only
curl -X POST "https://api.tcggraph.com/v1/webhooks" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/tcggraph-docs","events":["set.published","card.legality.changed"],"games":["pokemon"],"description":"docs example"}'const response = await fetch("https://api.tcggraph.com/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/tcggraph-docs",
"events": [
"set.published",
"card.legality.changed"
],
"games": [
"pokemon"
],
"description": "docs example"
}),
});
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/webhooks",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"url": "https://example.com/tcggraph-docs", "events": ["set.published", "card.legality.changed"], "games": ["pokemon"], "description": "docs example"},
)
body = response.json()
response.raise_for_status(){
"data": {
"id": "c3290649-ffc6-467c-9470-c5307925ca78",
"url": "https://example.com/tcggraph-docs",
"events": [
"set.published",
"card.legality.changed"
],
"games": [
"pokemon"
],
"description": "docs example",
"status": "active",
"disabledAt": null,
"disabledReason": null,
"failures": 0,
"lastDeliveryAt": null,
"lastStatusCode": null,
"createdAt": "2026-09-24T07:47:43.874Z",
"secret": "whsec_••••••••••••"
}
}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. details.field names it and details.allowed lists the choices where there are some. |
| 409 | conflict | The URL is already registered on this account. |
Unknown event
curl -X POST "https://api.tcggraph.com/v1/webhooks" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/tcggraph-docs-2","events":["card.reprinted"]}'const response = await fetch("https://api.tcggraph.com/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/tcggraph-docs-2",
"events": [
"card.reprinted"
]
}),
});
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/webhooks",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"url": "https://example.com/tcggraph-docs-2", "events": ["card.reprinted"]},
)
body = response.json()
response.raise_for_status(){
"error": {
"code": "invalid_request",
"message": "Unknown event \"card.reprinted\".",
"details": {
"field": "events",
"allowed": [
"set.published",
"card.created",
"card.updated",
"card.legality.changed",
"price.threshold.crossed"
]
}
}
}Same URL twice
curl -X POST "https://api.tcggraph.com/v1/webhooks" \
-H "Authorization: Bearer $TCGGRAPH_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/tcggraph-docs","events":["set.published"]}'const response = await fetch("https://api.tcggraph.com/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TCGGRAPH_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"url": "https://example.com/tcggraph-docs",
"events": [
"set.published"
]
}),
});
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/webhooks",
headers={"Authorization": f"Bearer {os.environ['TCGGRAPH_API_KEY']}"},
json={"url": "https://example.com/tcggraph-docs", "events": ["set.published"]},
)
body = response.json()
response.raise_for_status(){
"error": {
"code": "conflict",
"message": "That URL is already registered on this account.",
"details": {
"endpoints": "GET /v1/webhooks lists what is already registered."
}
}
}