Endpoint reference
Base URL: https://phantombot.gg/api/v1
Every endpoint requires Authorization: Bearer phk_live_…. The scope column tells you which permission the token needs — see Authentication.
Collection endpoints accept ?page=1&per_page=25. per_page is capped at 100; the default is 25. Responses include has_more so you can drive pagination without parsing Link headers.
Meta
| Method | Path | Scope | What it does |
|---|---|---|---|
GET | /me | — | The token's metadata (id, name, scopes, last-used). |
GET | /health | — | Liveness probe. Returns { "status": "ok" }. |
GET /me
curl https://phantombot.gg/api/v1/me \
-H "Authorization: Bearer phk_live_…"{
"token": {
"id": "42",
"name": "Production deploy script",
"preview": "phk_live_AbCdEfGh…0123",
"guild_id": "123456789012345678",
"scopes": ["cases:read", "cases:write"],
"expires_at": null,
"created_at": "2026-05-24T10:30:00+00:00",
"last_used_at": "2026-05-24T11:15:42+00:00"
},
"api": { "version": "v1", "env": "live" }
}Guild
| Method | Path | Scope | |
|---|---|---|---|
GET | /guild | guild:read | Headline metadata. |
GET | /guild/settings | guild:read | Module toggles + a few public knobs. |
Cases
| Method | Path | Scope | |
|---|---|---|---|
GET | /cases | cases:read | List cases. Filters: user_id, type, status, since. |
GET | /cases/{id} | cases:read | A single case. |
POST | /cases | cases:write | Create a note or warn case. |
PATCH | /cases/{id} | cases:write | Edit a case's reason and/or expires_at. |
Create a case
curl -X POST https://phantombot.gg/api/v1/cases \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"user_id": "234567890123456789",
"moderator_id": "345678901234567890",
"type": "warn",
"reason": "Posting referral spam"
}'type is restricted to note or warn via this endpoint — punishments that change Discord state (kick, ban, timeout, mute) are not exposed yet. Use the dashboard or wait for the next release.
Emits case.created to subscribed webhooks.
Edit a case
curl -X PATCH https://phantombot.gg/api/v1/cases/12345 \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "reason": "Updated after appeal review", "expires_at": "2026-09-01T00:00:00Z" }'- Send
reason(string, max 1000),expires_at(ISO 8601 ornullto clear), or both — at least one is required. statusandtypeare not editable here: status flips imply Discord-side action (unban, un-timeout) the API can't perform yet.- Cases with status
deletedare read-only and return422 case_deleted.
Emits case.updated.
Economy
| Method | Path | Scope | |
|---|---|---|---|
GET | /economy/wallet/{userId} | economy:read | Wallet for one member. Returns a zero-balance shell if they haven't earned anything yet. |
GET | /economy/leaderboard | economy:read | Top wallets, descending. |
POST | /economy/adjust | economy:write | Atomic balance change with a ledger row. |
Adjust balance
curl -X POST https://phantombot.gg/api/v1/economy/adjust \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "user_id": "234567890123456789", "delta": 500, "reason": "Tournament prize" }'deltais integer, can be negative.- Negative deltas that would push the balance below zero return
422 balance_would_be_negative. - Every adjustment writes a row to the economy ledger with
note: "api:token=<your token id>"so audits can trace back to source.
Emits economy.balance_changed.
Levelling
| Method | Path | Scope | |
|---|---|---|---|
GET | /levelling/user/{userId} | levelling:read | One member's XP + level + progress to next level. |
GET | /levelling/leaderboard | levelling:read | Top members by XP. |
POST | /levelling/adjust | levelling:write | Add/remove XP (delta) or set it absolutely (total_xp). |
The user endpoint returns a sensible zero-shell for members who haven't sent a message yet (level 0, full bar empty).
Adjust XP
# Relative — add or remove XP
curl -X POST https://phantombot.gg/api/v1/levelling/adjust \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "user_id": "234567890123456789", "delta": 500 }'
# Absolute — set total XP outright
curl -X POST https://phantombot.gg/api/v1/levelling/adjust \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "user_id": "234567890123456789", "total_xp": 12000 }'- Exactly one of
delta(integer, can be negative) ortotal_xp(integer ≥ 0) is required. - A
deltathat would push total XP below zero returns422 xp_would_be_negative. - The member's
levelis recomputed from the shared level curve, so the response reflects the new level immediately. - Level roles: the bot evaluates level-role grants the next time the member earns XP (message/voice) — an API adjustment updates the stored level at once but does not push role changes to Discord by itself.
Emits xp.level_up when the adjustment raises the member's level.
Tickets
| Method | Path | Scope | |
|---|---|---|---|
GET | /tickets | tickets:read | List tickets. Filters: status, opener_id, claimed_by_id. |
GET | /tickets/{id} | tickets:read | A single ticket. |
POST | /tickets/{id}/message | tickets:write | Post a staff message (or internal note) into a ticket. |
POST | /tickets/{id}/close | tickets:write | Close an open ticket. |
Post a message
curl -X POST https://phantombot.gg/api/v1/tickets/42/message \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "content": "We are looking into this now." }'content— required, max 2000 characters (Discord's message limit).internal— optional boolean.truestores the message as an internal staff note on the transcript only; nothing is posted to Discord and the ticket opener never sees it. Internal notes work on closed tickets too.author_id— optional Discord snowflake for attribution on internal notes.- Default (non-internal) messages are delivered to the ticket's Discord channel as the bot — Discord doesn't let us impersonate a staff member over REST. The delivery uses the exact same path as the dashboard's canned-response sender and is mirrored into the transcript immediately.
- Non-internal messages require the ticket to be
open(422 ticket_not_open) and to have a Discord channel (422 no_ticket_channel). If Discord refuses the send (bot missing permissions), you get502 discord_send_failed.
Returns 201 with the stored transcript message. Emits ticket.message.
Close a ticket
curl -X POST https://phantombot.gg/api/v1/tickets/42/close \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "reason": "Resolved via API integration" }'reasonis optional (max 1000 chars) — unless the guild has Require close reason enabled in ticket settings, in which case omitting it returns422 close_reason_required.- Only
opentickets can be closed (422 ticket_not_open). - The close follows the same path as the dashboard: the bot is notified instantly (Postgres NOTIFY) and archives/deletes the Discord channel per the guild's ticket settings, and a
closesystem entry is appended to the transcript.
Returns the updated ticket. Emits ticket.closed.
Giveaways
| Method | Path | Scope | |
|---|---|---|---|
GET | /giveaways | giveaways:read | List giveaways. Filter: status. |
GET | /giveaways/{id} | giveaways:read | A single giveaway. |
POST | /giveaways/{id}/end | giveaways:write | End a running giveaway now (bot draws winners). |
POST | /giveaways/{id}/reroll | giveaways:write | Reroll winners on an ended giveaway. |
End a giveaway
curl -X POST https://phantombot.gg/api/v1/giveaways/7/end \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)"- Works on
active,paused, orscheduledgiveaways; anything else returns422 giveaway_not_running. - Returns
202 Acceptedwith the giveaway in statusdrawing— the winner draw is asynchronous. Same flow as the dashboard's "End now": the bot worker is notified instantly, draws the winners, and posts the result. PollGET /giveaways/{id}untilstatusisended.
Reroll winners
curl -X POST https://phantombot.gg/api/v1/giveaways/7/reroll \
-H "Authorization: Bearer phk_live_…" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{ "count": 1, "reason": "Winner did not claim" }'- Only
endedgiveaways can be rerolled (422 giveaway_not_ended). count(default 1) — how many winners to replace. The firstcountwinners are replaced; the rest carry over, so rerolling 1 of 3 keeps the other two.exclude_user_ids— optional extra exclusions on top of the automatic previous-winner exclusion.- The draw is the same weighted (entry-count-proportional) selection the dashboard uses. If no eligible entries remain you get
422 not_enough_entries. - The response includes the full new
winnersarray. The bot posts the reroll update in Discord automatically.
Emits giveaway.rerolled.
Phantom Network
| Method | Path | Scope | |
|---|---|---|---|
GET | /network/lookup/{userId} | network:read | Cross-guild ban-intel lookup. |
Returns { "data": { "listed": false } } for clean users and the full public hit summary (trust score, report count, category, severity, first/last report) for listed ones.
Response envelopes
Collections always look like this:
{
"data": [ /* … */ ],
"page": 1,
"per_page": 25,
"has_more": true
}Singletons look like this:
{ "data": { /* … */ } }Errors:
{ "error": { "code": "…", "message": "…" } }