Webhooks reference

Embed
Preview of the card
<a href="https://coolgptgames.com/docs/webhooks-reference"><img src="https://coolgptgames.com/docs/_api/public/orgs/062b11d9-2b86-4627-9e86-96816a67d7a1/spaces/07d97e67-936e-4223-9eaf-6998a46fee24/pages/f485a669-0613-4ce1-bd41-115030b556d3/social-card.svg?v=3e7ae33eec8b" alt="Webhooks reference" width="600" height="315"></a>

Compact reference for outbound webhooks. For setup walkthroughs, full payload examples and verification code, see Webhooks.

Base URL: https://api.coolgptgames.com. Auth: Authorization: Bearer <key> with the webhooks scope, or a signed-in dashboard session. See API keys & scopes.

Management endpoints

Method & path

Body

Success response

Notes

POST /v1/dev/webhooks

{ "url": string, "events"?: string[] }

201 { webhook: { id, url, events, secret }, availableEvents }

url: valid URL, https:// only, ≤ 500 chars. Leave events out or send [] to subscribe to all (stored as ["*"]). "*" isn't accepted inside events. secret is returned only here.

GET /v1/dev/webhooks

—

200 { webhooks: [{ id, url, events, active, at, disabledAt, disabledReason, consecutiveFailures, failingSince }], availableEvents }

Newest first. at = creation time. No secrets. disabledReason is manual or failing.

PATCH /v1/dev/webhooks/:id

{ "active"?: boolean, "url"?: string, "events"?: string[] }

200 { webhook }

Switch on/off or edit. Re-enabling clears the failure streak and resumes queued deliveries. Empty body or unknown keys → 400.

DELETE /v1/dev/webhooks/:id

—

200 { ok: true }

Stops deliveries immediately and removes the delivery log

GET /v1/dev/webhooks/:id/deliveries

—

200 { deliveries: [{ id, webhookId, event, status, ok, statusCode, error, attempts, maxAttempts, nextAttemptAt, lastAttemptAt, deliveredAt, createdAt, attemptLog[] }] }

The 25 most recent deliveries, newest first. status: pending | succeeded | failed.

POST /v1/dev/webhooks/:id/deliveries/:deliveryId/redeliver

—

200 { ok: true, delivery }

Sends again now, same delivery id and body. { id, status: "pending", queued: true } if the worker is already sending it.

POST /v1/dev/webhooks/:id/test

—

200 { ok: true, delivery }

Sends webhook.test to this webhook only, whatever its event list. One attempt, no retries.

There's no endpoint to rotate a secret — see the rotation recipe under Caveats.

Errors

Status

error.code

When

400

validation_error

Bad url (not https, invalid, too long) or an unknown event name in events

400

too_many_webhooks

You already have 10 webhooks

401

unauthorized

Missing or invalid credentials

403

forbidden

The key lacks the webhooks scope, or the webhook belongs to another account

404

not_found

No webhook with that ID, or no such delivery

409

webhook_disabled

The webhook is switched off — test/redeliver after re-enabling it

409

delivery_not_redeliverable

The delivery predates retry support, so its body wasn't stored

Errors use the standard envelope { "error": { "code", "message", "details"? } }. See Errors & limits.

Limits

Limit

Value

Webhooks per account

10

URL

https:// only, ≤ 500 characters

Delivery timeout

10 seconds per attempt

Attempts per delivery

7 (1 + 6 retries) over about 34 hours

Auto-disable

Every attempt failing for 3 days and ≥ 5 consecutive failed deliveries

Delivery log shown

25 most recent deliveries, each with its attempt log

Delivery retention

Finished deliveries 30 days; pending ones kept

Event catalogue

Deliveries only cover your own games and the tournaments you created.

Event

Sent when

data fields

game.approved

A version of your game is approved, by the automated pipeline or a human moderator

gameId, gameVersionId, slug, title, reasonCodes: string[], automated: boolean

game.rejected

A version of your game is rejected, by the automated pipeline or a human moderator

gameId, gameVersionId, slug, title, reasonCodes: string[], automated: boolean

game.reported

A player files an abuse report on your game

gameId, reasonCode

review.created

A player submits or edits a rating with written text

gameId, rating (1–5), title (string or null), body

tournament.ended

A tournament you created settles (a background job checks every 30 seconds; viewing it after its end time also settles it)

tournamentId, name, gameId, winners: [{ userId, rank, prizeCoins }]

payout.paid

A creator payout is transferred to you

payoutId, amountUsd (number), currency ("usd"), periodStart, periodEnd, paidAt, transferId

webhook.test

You trigger a test for one webhook (not subscribable by name; sent only to that webhook)

message

automated is true for a pipeline decision and false for a human one. A version held for human review sends nothing until a moderator decides. A rejected update to a live game only rejects that version; the game stays published on its previous version. Canceled tournaments don't send tournament.ended. In winners, entrants who never scored are left out, and players tied on the same result share a rank and split the prizes for the places they occupy.

reasonCode / reasonCodes values: nsfw_sexual, nsfw_nudity, graphic_violence, gore, hate_symbols, harassment, ip_infringement, trademark, broken_not_playable, blank_screen, not_a_game, low_effort_duplicate, exact_duplicate, malicious_code, cryptominer, data_exfiltration, phishing_ui, misleading_metadata, spam, contact_info_spam, gambling, age_inappropriate, economy_abuse, other.

prizeCoins are virtual coins, not real money.

Request

POST <your url>
content-type: application/json
user-agent: CoolGPTGames-Webhooks/1.0
x-arcadey-event: <event>
x-arcadey-signature: sha256=<64 lowercase hex chars>
x-arcadey-delivery: <delivery id, stable across retries>
x-arcadey-attempt: <1-based attempt number>

Body (compact JSON, keys in this order):

{ "id": "<delivery id>", "event": "<event>", "data": { }, "sentAt": "<ISO 8601 UTC>" }

sentAt is when the event was queued and does not change between retries.

Signature algorithm

Item

Value

Header

x-arcadey-signature

Format

sha256= + lowercase hex digest

Algorithm

HMAC-SHA256

Key

The full secret string as UTF-8, including the whsec_ prefix

Message

The raw request body bytes, exactly as received

Timestamp in signature

None. Use the signed sentAt field for replay checks.

Comparison

Constant-time (crypto.timingSafeEqual / hmac.compare_digest)

// Node.js
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
# Python
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()

Secrets look like whsec_ + 48 hex characters, and each webhook has its own.

Delivery and retry schedule

Item

Value

Queueing

The event is stored when it happens; a worker sends it within ~5 seconds

Success

Any 2xx response within 10 seconds. Your response body is ignored.

Failure

Non-2xx, a 3xx redirect, timeout, or a network/TLS error

Attempts

7

Backoff after each failure

+1 min, +5 min, +30 min, +2 h, +8 h, +24 h

Redirects

Not followed — a 3xx fails the attempt

Manual redelivery

POST …/deliveries/:deliveryId/redeliver

Ordering

Not guaranteed. Subscribed webhooks receive the event in parallel.

Delivery ID

x-arcadey-delivery and the body's id, stable across retries — dedupe on it

Logged per attempt

attempt, statusCode (null if no response), ok, error (≤ 300 chars), durationMs, manual, nextRetryAt

Caveats

  • Dashboard with no events ticked: creates an all-events webhook, the same as leaving out events in the API.

  • Auto-disabled webhook: you get a dashboard notification (and an email if enabled). Re-enable with the toggle or PATCH /v1/dev/webhooks/:id once your endpoint is healthy.

  • Secret rotation: create a second webhook for the same URL, accept either secret, then delete the old one. Each event arrives twice during the overlap.

Updated