Disguild API v1 and webhooks
Read your own listings, reviews and analytics, and get signed webhooks when things happen. Create keys and webhooks in the API dashboard (sign in with Discord).
Authentication
Send Authorization: Bearer ga_live_.... A key is shown once when you create it. Only a hash is stored, so a lost key cannot be recovered: rotate it. Revoking or rotating works on the next request. You can have 5 active keys.
| Scope | Allows |
|---|---|
listings:read | GET /api/v1/listings, GET /api/v1/listings/{id} |
reviews:read | GET /api/v1/listings/{id}/reviews |
analytics:read | GET /api/v1/listings/{id}/stats?days=7, 30 or 90 |
A key only sees listings owned by its owner. Someone else's listing id returns 404, the same as a missing one.
Limits and errors
60 requests per minute per key. Every response carries x-request-id, x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (seconds). A 429 adds retry-after.
Errors look like { "error": { "code", "message", "requestId" } }. Codes: unauthorized 401, invalid_api_key 401, insufficient_scope 403, not_found 404, bad_request 400, rate_limited 429, unavailable 503.
Lists accept ?page=1&limit=25 (limit 1 to 50) and return { data, page, limit, total, has_more }. Single items return { "data": {...} }.
Endpoints
GET /api/v1/listings: your listings (id, type, slug, name, short_description, category, language, status, vote_count, created_at, updated_at).GET /api/v1/listings/{id}: one listing, same fields.GET /api/v1/listings/{id}/reviews: visible reviews with developer replies. Hidden reviews are never returned.GET /api/v1/listings/{id}/stats?days=30: totals (views, clicks, votes, reviews, clickRate), a daily series and traffic sources.
curl -H "Authorization: Bearer $GA_KEY" https://disguild.pages.dev/api/v1/listings
Webhooks
Up to 3 webhooks per user. Each receives the events it subscribes to for all of your listings. No voter or reviewer identity and no review text is ever sent.
| Event | Fired when |
|---|---|
listing.approved | A listing is approved, or restored after suspension |
listing.rejected | A listing is rejected |
listing.updated | An edit to an approved listing was approved and is now live (no text sent) |
review.created | A new review is posted (rating only, no text) |
vote.created | A vote is accepted (new vote count only) |
Example body:
{ "id": "evt_...", "type": "vote.created", "created_at": "ISO", "data": { "listing": { "id", "type", "slug", "name" }, "vote_count": 12 } } Headers and signature
x-disguild-id (the same on every retry), x-disguild-event, x-disguild-timestamp (unix seconds, refreshed on each attempt) and x-disguild-signature: v1=<hex>. The hex is HMAC-SHA256 of "<id>.<timestamp>.<raw body>" using your whsec_... secret. Verify against the raw body bytes.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(secret, headers, rawBody) {
const ts = headers['x-disguild-timestamp'];
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay protection
const expected = 'v1=' + createHmac('sha256', secret)
.update(`${headers['x-disguild-id']}.${ts}.${rawBody}`).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(headers['x-disguild-signature'] ?? '');
return a.length === b.length && timingSafeEqual(a, b);
} Idempotency: delivery is at least once. Store processed x-disguild-id values and ignore repeats.
Delivery and retries
Any 2xx is success. Anything else (non-2xx, an 8 second timeout, a network error) is retried with backoff at 30 s, 1 m, 2 m, 4 m and 8 m, up to 6 attempts, then marked dead. Redirects are not followed. After 5 dead deliveries in a row the webhook is disabled; any success resets the count. You can pause, resume, rotate the secret, delete and send a test delivery from the dashboard. Expect up to about a minute of delay. Delivery history is kept 30 days.
Allowed destinations
Webhook URLs must be https on port 443, with a public hostname (no IP addresses, no credentials, no localhost or internal-style names). Every address the name resolves to must be public, and this is checked again before each delivery.