Developer API
Read and manage your server's tickets and giveaways from your own code, and get a signed webhook when something happens.
Overview
The greed API lets your own website, bot or service work with your server's tickets and giveaways. You can:
- read tickets, their messages and transcripts, and reply to, claim or close them;
- start, end and reroll giveaways, read entries and claims, and set a private award;
- receive a webhook the moment a ticket opens or closes, or a giveaway picks a winner.
Everything is managed from the dashboard under Developer API. You need Manage Server.
The API is available to every server. Server Premium raises the limits, see Limits.
API keys
Create a key on the Keys tab of the Developer API page. Give it a name and pick its scopes:
| Scope | Allows |
|---|---|
tickets:read | Listing tickets, reading messages and transcripts, listing panels |
tickets:write | Replying to, claiming and closing tickets |
giveaways:read | Listing giveaways, reading entries and claims |
giveaways:write | Starting, ending and rerolling giveaways, setting awards |
The key is shown once, when you create it. Store it somewhere safe, such as an environment variable. If you lose it, rotate it.
- Rotate creates a new key with the same name and scopes. The old key keeps working for 24 hours, so you can switch over without downtime.
- Revoke stops a key immediately.
A key belongs to one server. Every change made with a key shows up in the server's audit log with the key's name.
Treat a key like a password. Never put it in a website, a public repository or a Discord message.
Making requests
Send the key as a Bearer token. All requests and responses are JSON.
curl https://x.greed.best/v1/tickets?status=open \
-H "Authorization: Bearer gk_live_..."
Successful responses look like { "data": ... }. Errors look like { "error": "...", "code": "TICKET_NOT_FOUND" } with a matching HTTP status. All ids are strings.
GET /v1/me returns the key's server, name and scopes, which is a quick way to check a key works.
Tickets
A ticket id is the id returned by the list endpoint or carried by a ticket webhook.
Giveaways
A giveaway's id is the id of its message.
Webhooks
Add an endpoint on the Webhooks tab, pick the events it should receive, and greed sends a POST to it whenever one of them happens. The endpoint must be a public https:// address.
Events
| Event | When |
|---|---|
ticket.opened | A ticket was opened |
ticket.claimed | A staff member claimed a ticket |
ticket.closed | A ticket was closed, by a person or automatically |
ticket.reopened | A closed ticket was reopened |
ticket.deleted | A ticket was deleted |
ticket.transcript | A transcript was saved, with its link |
giveaway.started | A giveaway was posted |
giveaway.ended | A giveaway ended, with its winners |
giveaway.winner_selected | Winners were picked, after an end, a reroll or a missed claim |
giveaway.claimed | A winner claimed their prize |
giveaway.claim_expired | A winner did not claim in time |
giveaway.rerolled | Someone rerolled a giveaway |
Every request has the same shape:
{
"id": "evt_6f1c9e3a2b8d4f0e9a7c5b3d1e2f4a6b",
"type": "giveaway.winner_selected",
"createdAt": "2026-10-28T18:04:11.512Z",
"guildId": "123456789012345678",
"data": {
"id": "1300000000000000000",
"prize": "Discord Nitro",
"winners": ["987654321098765432"],
"claimDeadline": "2026-10-28T19:04:11.000Z",
"reason": "ended"
}
}
Ticket events carry the same ticket object as GET /v1/tickets/{id}. An event can occasionally arrive twice, so use id to ignore one you have already handled.
The Send test button sends a ping event so you can check your endpoint.
Verifying the signature
Each endpoint has a signing secret (whsec_...), shown once when you add the endpoint or rotate it. Every request carries a greed-signature header:
greed-signature: t=1761674651,v1=5f2b...e91c
v1 is an HMAC-SHA256 of {t}.{raw request body} using your secret. Always check it, and refuse requests older than 5 minutes so an old request cannot be replayed.
import crypto from "node:crypto";
export function verifyGreed(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const age = Math.abs(Date.now() / 1000 - Number(parts.t));
if (!parts.t || !parts.v1 || age > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
Verify against the raw body exactly as received. Parsing the JSON and re-serializing it changes the bytes and the signature will not match.
Responding and retries
Answer with any 2xx status within 10 seconds. If your endpoint times out, can't be reached, or answers 408, 429 or 5xx, greed tries again after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Other 4xx answers are not retried.
The Deliveries list on each endpoint shows the last 50 deliveries with their status and the HTTP status your endpoint answered with, and lets you send one again. greed never stores the body of your response. After 50 failed delivery attempts in a row the endpoint is turned off; turn it back on from the dashboard once it is fixed.
Limits
| Free | Server Premium | |
|---|---|---|
| API keys | 5 | 25 |
| Webhook endpoints | 3 | 10 |
| Webhook deliveries per day | 1,000 | 50,000 |
| Requests per minute, per key | 60 | 300 |
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Over the limit you get 429 with a Retry-After header. Deliveries past the daily limit are skipped and shown as skipped in the deliveries list; the count resets at midnight UTC.
Delivery history is kept for 7 days.