Tickets
A preset-first support ticket system: one panel, several ticket types, each with its own greeting, category, naming and limits.
The ticket system is preset-first: one panel can carry several buttons, or a dropdown, and each one opens a different kind of ticket: its own greeting, category, channel naming, per-type limit and up to five opening questions. A preset is a nullable overlay on the guild's own ticket defaults: anything a preset doesn't set falls back to whatever the server has configured generally.
Post the panel
Syntax
,ticket setup [embed]
Example
,ticket setup {description: Need help? Click below.}Posts the public panel in the current channel: one Open button per enabled preset, in the order
you arranged them, or a single dropdown if the server is set to dropdown mode. A server with no
presets enabled still gets a plain Ticket button that opens against the guild defaults, so
,ticket setup always posts something clickable.
Create a preset
Syntax
,ticket preset new <label>
Example
,ticket preset new Support
Syntax
,ticket preset set <type> <field> [value]
Example
,ticket preset set support category #tickets
Syntax
,ticket preset show <type>
Example
,ticket preset show support
Syntax
,ticket preset list
A blank value on set clears a value, channel or role field back to inherit: the preset
stops overriding that setting and falls back to the guild default again. It does not work on every
field: a toggle refuses a blank and asks for on or off, the three-state toggles want the literal
word inherit, and style and label refuse an empty value outright. Blank clears the fields that
are allowed to hold nothing. There's also a full Components V2 manager at
,ticket panel for anyone who'd rather click through greeting text and questions than type them,
and both surfaces read the same allowlist, so they can never disagree about what a field means. One
field is panel-only on purpose: a preset's own blacklist. Ask for it on the command line and you
get a pointer back to the panel instead of a list, for the reason
the blacklist section gives. Everything else is the same
field whichever way you set it.
Tip
Everything a preset can override is a guild default with an escape hatch: set nothing on a preset and it behaves exactly like the plain guild-wide ticket flow. This is worth remembering when a preset "isn't working": it's often that the guild default was never set either.
Two numbers, one ticket
Every ticket carries two numbers, and mixing them up is the most common point of confusion here.
- The serial is guild-wide: every ticket the server has ever opened, in one running count.
It's the identity: internal lookups and reopening always ultimately resolve to this number, and in
an embed code it's
{ticket.serial}. - The display number is per-type, and what members actually see in the channel name:
support-1,billing-1, andticket-412from before presets existed can all legitimately exist side by side, because each type counts its own tickets separately. This is the one{ticket.id}fills in, so the token named "id" is deliberately not the identity.
Which number you reach for depends on what you're actually doing, and ,ticket reopen is where
that bites. It is one command carrying two different transitions:
Syntax
,ticket reopen [type] [number]
Example
,ticket reopen support 3
- No arguments, run inside the ticket's own channel: reopens an archived ticket in place. The channel never went anywhere, so it moves back to the open category and whoever opened it gets their access re-applied. Nothing is lost. The 🔓 button inside the ticket does the same job, but only if you have turned the control row on, and it is off until you do.
- With a number: recreates a deleted ticket. Its channel is gone, so the bot builds a new one under the original number: ticket 12 comes back as ticket 12, with no message history, because Discord cannot undelete a channel. The reply says so and points at the transcript in the log channel, which is the only record of what was said in the old one.
So a number is not a way to reopen an archive. Give one for a ticket that is merely archived and
the bot tells you so in as many words: "Ticket #3 is archived, not deleted — reopen it from
inside its own channel." That sentence is an instruction: go to the channel and run
,ticket reopen with nothing after it. A number for a ticket that is still open gets its own
refusal, and a number nothing matches gets a third.
On the recreate form, a bare number is usually enough: when exactly one deleted ticket carries that
number the bot simply rebuilds it. It only asks when the number belongs to more than one type's
deleted tickets: then it lists the candidates with their guild-wide serials and tells you what to
type instead, because a display number is only unique within its type while the serial always
resolves on its own. Naming the type up front, as in ,ticket reopen support 3, skips the question
entirely.
Archived means the channel is still there; deleted means the number is all that is left.
Two embed codes, easily confused
There are two separate custom embed codes in play, and they render in different places:
- The panel embed (set with
,ticket setup) is the message carrying the Open button(s). It only ever renders once, on the panel message itself. - The greeting (set with
,ticket message) is what posts inside a new ticket once it opens. The guild has one default greeting, and any preset can override it with its own through,ticket preset set <type> greeting <code>.
Editing one never touches the other: a preset that changes its greeting doesn't change what the public panel looks like, and vice versa.
A greeting that pings whoever opened the ticket, names the type and shows the number the channel is named after:
Syntax
,ticket message <code>
Example
,ticket message {color: #fb4868}$p{title: {ticket.type} — ticket #{ticket.id}}$p{description: {ticket.opener} thanks for getting in touch — staff have been notified and will answer in this channel.}$p{field: Opened && {ticket.created} && inline}$p{footer: {guild.name}}A panel that says what the button underneath it is for:
Syntax
,ticket setup [embed]
Example
,ticket setup {color: #fb4868}$p{title: Need a hand?}$p{description: Open a ticket below and the {guild.name} staff team will pick it up — please don't DM or ping staff directly.}$p{thumbnail: {guild.icon}}Both need Manage Server, and neither is behind premium. Three things are worth copying out of them:
{ticket.*}fills in on the greeting, and in no other embed code. A panel exists before any ticket does, so there is nothing for{ticket.id}to read, and an unresolved variable doesn't come out blank, it posts its own name. Written into a panel code,{ticket.id}posts the literal textticket.id, and,customembed, the close log and the archive log all do the same. General variables like{user},{guild.name}and{color.random}work on both.{ticket.id}is the number members see,{ticket.serial}is the one that's unique. They are the two numbers above: use the first in a title people read, the second when you want the number,ticket reopennever has to ask about. If the type asks questions,{ticket.answer1}through{ticket.answer5}drop the answers straight into the greeting. A ticket token that resolves to nothing ({ticket.claimer}before anyone claims it,{ticket.answer3}on a type that asks two questions) is the other case, and that one really is empty: inside a{field:}the empty half is backfilled with an invisible character so the field still renders rather than breaking the embed, and anywhere else it simply contributes nothing. Empty is what a resolved token with no value looks like; its own name is what an unresolved one looks like.- The greeting honors
++delete <seconds>; the panel ignores it. The flag is stripped out of the code either way, so it never prints as text, but only the greeting schedules anything: a panel that deleted itself would take its own Open button with it.
One surface outside the embed codes understands ticket tokens too: a preset's channel-name
template, set with ,ticket preset set <type> channelname. It has its own smaller set:
{ticket.num}, {ticket.id}, {ticket.type} and {ticket.opener}, plus {guild.name},
{guild.id}, {guild.membercount}, {num}, {preset} and {user}. It builds a channel
name rather than an embed, so the result is lowercased, spaces become hyphens, and anything Discord
won't accept in a channel name is dropped. {ticket.opener} there is the opener's username, not a
ping, and {ticket.num} lives only there: write it into a greeting and it comes out empty, because
the greeting answers any {ticket.…} it doesn't recognise with nothing at all.
The panel is also rendered exactly once, when you post it, so every variable in it is frozen at that
moment: {guild.membercount} on a panel is the count from the day you ran ,ticket setup, not
today's. The same code can never serve both surfaces: everything that makes a greeting worth
reading is written about a ticket that, on the panel, does not exist yet.
Tip
Try either code with ,customembed
first and you'll see the difference immediately: the general variables fill in, and every
{ticket.*} renders as its own name, because ,customembed has no ticket either. The language
itself (every tag, $p, buttons, Components V2) is in
embed codes.
Auto-archive is opt-in twice
Syntax
,ticket autoclose <hours|off>
Example
,ticket autoclose 72
This is the server-wide window: a ticket with no activity for this many hours gets a warning
posted in-channel, then, if still silent 24 hours after that, is archived, never deleted. A
preset can carry its own override on top of the server setting through ,ticket preset set <type> autoclose <hours>: leaving it blank means "inherit the server setting," and setting it to 0
switches auto-archive off for that one type even while the rest of the server has it on.
It warns first, archives later, and archives rather than closes
A ticket vanishing on someone mid-conversation is worse than a stale one sitting around, so the sweep always posts a warning before it acts, and waits a further 24 hours before archiving. Archive means the channel is locked and moved out of the way, not deleted: everything is still there and can be reopened.
Two blacklists: don't conflate them
Syntax
,ticket blacklist <add|remove|list> [@role/@user]
Example
,ticket blacklist add @spammer
This is the ticket blacklist, scoped to this server only: who may not open a ticket here. It blocks opening and nothing else: it doesn't eject anyone from a ticket they already have. A blocked member's refusal never names which entry matched, so a blocked user can't work out which of their roles is the reason; the staff-facing log entry does name it. A per-type block on top of the server-wide list is set from the ticket panel rather than this command, and its editors there are ephemeral for the same reason: a blocked member should never be able to read out which role blocked them from a message only they can see.
This is a completely different list from the bot's own global blacklist. That one is
,blacklist, and it is owner-only: nobody running a server can use it, on any server, so it is
never the tool for a problem member here. It stops the person interacting with Righteous at all,
everywhere the bot is, and that does include opening a ticket.
The two are still easy to tell apart, because they answer differently. The global list replies "You cannot use this bot.", on a slash command, on any button, and on the ticket Open button. The server-level list above replies "You are not allowed to open tickets in this server." Read the wording and you know which one you are looking at.
One quirk worth knowing before you conclude nothing happened: on prefix commands the global list
says nothing at all and simply ignores the message. Slash commands and buttons always answer. So a
globally blacklisted member typing ,help sees silence, and the same member clicking Open sees a
refusal.
Turn on the in-ticket control row
Syntax
,ticket buttons <on|off>
Example
,ticket buttons on
Defaults off. When enabled, every new ticket gets a row of buttons for the common actions: claim, transfer, add/remove a member, rename, close, archive and so on. Staff don't need to remember the matching slash or prefix commands. It's off by default because not every server wants the extra row taking up space in a ticket channel that a staff member who already knows the commands doesn't need.
Transcripts
Syntax
,ticket transcript <on|off>
Example
,ticket transcript on
On by default. Closing or archiving a ticket attaches an HTML transcript of the conversation to the
log message: as a file attachment, not a link, since a link to hosted transcript media can go
stale once its signed URL expires while an attached file doesn't. Turning this off stops the
automatic attachment, but you can still pull one on demand for a specific ticket with ,ticket log,
and a preset can turn transcripts back on for its own type even while the server default is off.
Common issues
A preset doesn't seem to change anything. Check that the field is actually set on the preset and not blank: a blank value means "inherit," so an unset field silently falls through to the guild default, which may not be what you expected either.
,ticket reopen 3 says the ticket is archived, not deleted. The numbered form only rebuilds a
ticket whose channel is gone. An archived ticket still has its channel, so it's reopened from
inside it: go to the channel and run ,ticket reopen with nothing after it. The 🔓 button does the
same thing on a server that has turned the control row on; on
a default server the command is the only route.
Reopening a deleted ticket by number asks me which type I meant. That happens when more than one
deleted ticket carries that number: display numbers only guarantee uniqueness within one type. Run
it again with the type in front (,ticket reopen support 3), or with the guild-wide serial the bot
listed beside each candidate.
A member says they're blocked from opening a ticket but won't say why. They can't: the ticket
blacklist deliberately never tells the blocked member which entry matched, so they can't work out
which role or membership got them blocked. Check the staff-facing log, or ,ticket blacklist list,
to see the actual entry.
Auto-archive fired and nobody got a warning first. It shouldn't: the sweep always posts a warning and waits 24 hours before archiving. If a ticket skipped straight to archived, check whether it was manually archived instead, which doesn't go through the same warning step.
Nothing here fixed it. The shared common issues page works the same ground from the symptom end.