Skip to content

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

MethodPathScopeWhat it does
GET/meThe token's metadata (id, name, scopes, last-used).
GET/healthLiveness probe. Returns { "status": "ok" }.

GET /me

bash
curl https://phantombot.gg/api/v1/me \
  -H "Authorization: Bearer phk_live_…"
json
{
  "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

MethodPathScope
GET/guildguild:readHeadline metadata.
GET/guild/settingsguild:readModule toggles + a few public knobs.

Cases

MethodPathScope
GET/casescases:readList cases. Filters: user_id, type, status, since.
GET/cases/{id}cases:readA single case.
POST/casescases:writeCreate a note or warn case.
PATCH/cases/{id}cases:writeEdit a case's reason and/or expires_at.

Create a case

bash
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

bash
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 or null to clear), or both — at least one is required.
  • status and type are not editable here: status flips imply Discord-side action (unban, un-timeout) the API can't perform yet.
  • Cases with status deleted are read-only and return 422 case_deleted.

Emits case.updated.

Economy

MethodPathScope
GET/economy/wallet/{userId}economy:readWallet for one member. Returns a zero-balance shell if they haven't earned anything yet.
GET/economy/leaderboardeconomy:readTop wallets, descending.
POST/economy/adjusteconomy:writeAtomic balance change with a ledger row.

Adjust balance

bash
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" }'
  • delta is 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

MethodPathScope
GET/levelling/user/{userId}levelling:readOne member's XP + level + progress to next level.
GET/levelling/leaderboardlevelling:readTop members by XP.
POST/levelling/adjustlevelling:writeAdd/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

bash
# 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) or total_xp (integer ≥ 0) is required.
  • A delta that would push total XP below zero returns 422 xp_would_be_negative.
  • The member's level is 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

MethodPathScope
GET/ticketstickets:readList tickets. Filters: status, opener_id, claimed_by_id.
GET/tickets/{id}tickets:readA single ticket.
POST/tickets/{id}/messagetickets:writePost a staff message (or internal note) into a ticket.
POST/tickets/{id}/closetickets:writeClose an open ticket.

Post a message

bash
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. true stores 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 get 502 discord_send_failed.

Returns 201 with the stored transcript message. Emits ticket.message.

Close a ticket

bash
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" }'
  • reason is optional (max 1000 chars) — unless the guild has Require close reason enabled in ticket settings, in which case omitting it returns 422 close_reason_required.
  • Only open tickets 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 close system entry is appended to the transcript.

Returns the updated ticket. Emits ticket.closed.

Giveaways

MethodPathScope
GET/giveawaysgiveaways:readList giveaways. Filter: status.
GET/giveaways/{id}giveaways:readA single giveaway.
POST/giveaways/{id}/endgiveaways:writeEnd a running giveaway now (bot draws winners).
POST/giveaways/{id}/rerollgiveaways:writeReroll winners on an ended giveaway.

End a giveaway

bash
curl -X POST https://phantombot.gg/api/v1/giveaways/7/end \
  -H "Authorization: Bearer phk_live_…" \
  -H "Idempotency-Key: $(uuidgen)"
  • Works on active, paused, or scheduled giveaways; anything else returns 422 giveaway_not_running.
  • Returns 202 Accepted with the giveaway in status drawing — 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. Poll GET /giveaways/{id} until status is ended.

Reroll winners

bash
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 ended giveaways can be rerolled (422 giveaway_not_ended).
  • count (default 1) — how many winners to replace. The first count winners 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 winners array. The bot posts the reroll update in Discord automatically.

Emits giveaway.rerolled.

Phantom Network

MethodPathScope
GET/network/lookup/{userId}network:readCross-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:

json
{
  "data":     [ /* … */ ],
  "page":     1,
  "per_page": 25,
  "has_more": true
}

Singletons look like this:

json
{ "data": { /* … */ } }

Errors:

json
{ "error": { "code": "…", "message": "…" } }

Phantom is a product of Hydra Labs. The bot is run as a managed service; you do not need to host it yourself.