Alert Templates (DTS)¶
DTS stands for Dynamic Template Strings -- the system that controls how Dexter formats every alert message it sends. Each alert type (monsters, raids, eggs, quests, invasions, lures, nests, weather changes, gym updates, fort updates, and max battles) has its own template that you can fully customise.
Templates use Handlebars syntax. Variables are wrapped in {{ }} and rendered at send time with live data from the webhook event.
File structure¶
The main DTS configuration lives in config/dts.json. Dexter ships a default version in config/defaults/dts.json -- if you do not have your own dts.json, the default is copied into config/ on first run.
You can also split templates across multiple files by placing additional .json files in the config/dts/ directory. Dexter loads and merges all of them, so you can organise by language, alert type, or platform.
Additionally, any template string field can reference an external text file with the @include directive:
Dexter will look for mon.txt inside the config/dts/ folder, load its contents, and render them as a Handlebars template. This is useful for keeping complex description blocks readable and version-controlled separately.
Anatomy of a DTS entry¶
Each entry in dts.json (or a supplementary file) is a JSON object with these top-level fields:
{
"id": "standard",
"type": "monster",
"language": "en",
"default": true,
"platform": "discord",
"template": {
...
}
}
| Field | Description |
|---|---|
id |
Template identifier. Users can select a specific template with template<id> on tracking commands (e.g. !track everything iv100 templatehundo). |
type |
The alert type this template applies to. See Alert types below. |
language |
Language code (e.g. en, de, fr). Dexter matches templates whose language matches the user's configured locale, falling back to entries with no language set. |
default |
When true, this template is used if no specific template ID is matched. The first entry with default: true for a given type/platform/language wins. |
platform |
Either discord or telegram. |
template |
The message payload. Structure depends on platform. |
Alert types¶
| Type | Description |
|---|---|
monster |
Pokemon with IV data available |
monsterNoIv |
Pokemon without IV data (unscanned) |
monsterchange |
A spawn changed species (with IV) |
monsterchangeNoIv |
A spawn changed species (without IV) |
raid |
Active raid boss |
egg |
Raid egg (before hatching) |
quest |
Field research quest |
invasion |
Team Rocket invasion / Kecleon / Gold Stop / Showcase |
lure |
Active lure module |
nest |
Pokemon nest |
gym |
Gym team change or slot availability |
weatherchange |
Weather change affecting tracked pokemon |
fort-update |
Pokestop or gym added, removed, or edited |
maxbattle |
Dynamax / max battle at a Power Spot |
greeting |
Welcome message sent to newly registered users |
help |
Help message shown via the help command |
questdigest |
Summary of quests missed during quiet hours |
Platform template structures¶
Discord¶
Discord templates use the embed object format. A typical template:
"template": {
"embed": {
"color": "{{color}}",
"title": "{{fullName}} {{genderData.emoji}}",
"url": "{{{reactMapUrl}}}",
"description": "Your message content here",
"timestamp": "{{nowISO}}",
"footer": {
"text": "My Bot"
},
"thumbnail": {
"url": "{{{imgUrl}}}"
},
"image": {
"url": "{{{staticMap}}}"
}
}
}
For webhook channels (as opposed to bot DMs), you can also set username and avatar_url at the top level of the template to override the webhook identity per message:
Embeds can also include fields as an array and an author block.
Telegram¶
Telegram templates use a flat structure:
"template": {
"content": "Your message text with **Markdown**",
"parse_mode": "Markdown",
"sticker": "{{{stickerUrl}}}",
"location": true,
"webpage_preview": true
}
| Field | Description |
|---|---|
content |
The message body. Supports Markdown or HTML depending on parse_mode. |
parse_mode |
Markdown or HTML. |
sticker |
URL for a sticker image sent before the message. |
location |
true to include a Telegram location pin. |
webpage_preview |
true to allow link previews. Useful for embedding static map images. |
send_order |
Array to reorder message parts, e.g. ["sticker", "location", "text"]. Default order is sticker, text, location. |
Static maps in Telegram
To embed a map image, include a zero-width link at the start of your content:
Setwebpage_preview to true so Telegram renders the image.
Triple braces¶
Handlebars HTML-escapes values by default when you use {{value}}. For URLs and fields containing special characters (like <, >, &), use triple braces {{{value}}} to output the raw unescaped value. This is essential for:
{{{imgUrl}}}{{{staticMap}}}{{{googleMapUrl}}}{{{appleMapUrl}}}{{{reactMapUrl}}}{{{pokestopName}}}/{{{gymName}}}(may contain special characters)
Template variables by alert type¶
Common variables (available on most alert types)¶
These location and map variables are available on nearly all alert types:
| Variable | Description |
|---|---|
{{latitude}} |
Latitude of the location |
{{longitude}} |
Longitude of the location |
{{addr}} |
Formatted address string |
{{streetNumber}} |
Street number |
{{streetName}} |
Street name |
{{zipcode}} |
Postal / zip code |
{{city}} |
City name |
{{state}} |
State / region name |
{{stateCode}} |
2-letter state code |
{{country}} |
Country name |
{{countryCode}} |
2-letter country code |
{{neighbourhood}} |
Neighbourhood name |
{{flag}} |
Country flag emoji |
{{areas}} |
Comma-separated matched geofence area names |
{{matched}} |
Matched areas as an array (for iteration) |
{{distance}} |
Distance in metres from user location to the alert |
{{bearing}} |
Compass bearing from user location |
{{bearingEmoji}} |
Directional arrow emoji |
{{now}} |
Current timestamp (formatted) |
{{nowISO}} |
Current timestamp in ISO 8601 |
{{{staticMap}}} |
Static map image URL |
{{{googleMapUrl}}} |
Google Maps link |
{{{appleMapUrl}}} |
Apple Maps link |
{{{wazeMapUrl}}} |
Waze Maps link |
{{{reactMapUrl}}} |
ReactMap / web map link |
Monster (monster / monsterNoIv)¶
| Variable | Description |
|---|---|
{{pokemonId}} / {{id}} |
Pokemon ID number |
{{name}} |
Pokemon name (localised) |
{{nameEng}} |
Pokemon name (English) |
{{fullName}} |
Name including form (localised) |
{{formName}} |
Form name (localised) |
{{formNameEng}} |
Form name (English) |
{{formId}} |
Form ID number |
{{generation}} |
Generation number |
{{generationName}} |
Generation name (localised) |
{{generationRoman}} |
Roman numeral generation (I, II, III...) |
{{disguisePokemonName}} |
Disguise name (Ditto) |
{{encounterId}} |
Scanner encounter ID |
{{time}} |
Despawn time (formatted) |
{{tthh}} |
Hours until despawn |
{{tthm}} |
Minutes until despawn |
{{tths}} |
Seconds until despawn |
{{disappear_time}} |
Unix timestamp of despawn (for Discord <t:> formatting) |
{{confirmedTime}} |
Whether despawn time is verified |
{{iv}} |
IV percentage (2 decimal places) |
{{cp}} |
Combat Power |
{{level}} |
Pokemon level |
{{atk}} |
Attack IV (0-15) |
{{def}} |
Defence IV (0-15) |
{{sta}} |
Stamina IV (0-15) |
{{weight}} |
Weight in kg |
{{height}} |
Height |
{{size}} |
Size rating (1=XXS, 5=XXL) |
{{sizeName}} |
Size name |
{{gender}} |
Gender ID (0=unset, 1=male, 2=female, 3=genderless) |
{{genderData.name}} |
Gender name |
{{genderData.emoji}} |
Gender emoji |
{{quickMoveId}} |
Quick move ID |
{{quickMoveName}} |
Quick move name (localised) |
{{quickMoveNameEng}} |
Quick move name (English) |
{{quickMoveEmoji}} |
Quick move type emoji |
{{chargeMoveId}} |
Charge move ID |
{{chargeMoveName}} |
Charge move name (localised) |
{{chargeMoveNameEng}} |
Charge move name (English) |
{{chargeMoveEmoji}} |
Charge move type emoji |
{{color}} |
Embed colour (primary type) |
{{ivColor}} |
Embed colour based on IV perfection |
{{{imgUrl}}} |
Pokemon image URL |
{{{stickerUrl}}} |
Sticker image URL |
{{typeName}} |
Pokemon type (localised) |
{{emojiString}} |
Type emoji string |
{{rarityName}} |
Rarity tier (localised) |
{{rarityNameEng}} |
Rarity tier (English) |
{{shinyPossible}} |
true/false shiny availability |
{{shinyPossibleEmoji}} |
Sparkle emoji if shiny possible |
{{catchBase}} |
Base catch rate % |
{{catchGreat}} |
Great Ball catch rate % |
{{catchUltra}} |
Ultra Ball catch rate % |
Weather variables (monster):
| Variable | Description |
|---|---|
{{boosted}} |
Whether the Pokemon is weather boosted |
{{boostWeatherName}} |
Name of boosting weather |
{{boostWeatherEmoji}} |
Emoji for boosting weather |
{{gameWeatherName}} |
Current in-game weather name |
{{gameWeatherEmoji}} |
Current in-game weather emoji |
{{weatherChange}} |
Indicates a weather change may occur |
{{weatherCurrentName}} |
Current weather name |
{{weatherCurrentEmoji}} |
Current weather emoji |
{{weatherNextName}} |
Next weather forecast name |
{{weatherNextEmoji}} |
Next weather forecast emoji |
{{weatherChangeTime}} |
Time of weather change (HH:00) |
PVP variables (monster):
PVP data is available as arrays you iterate over:
| Variable | Description |
|---|---|
{{pvpLittle}} |
Array of Little League PVP entries |
{{pvpGreat}} |
Array of Great League PVP entries |
{{pvpUltra}} |
Array of Ultra League PVP entries |
{{pvpLittleBest}} |
Best Little League entry |
{{pvpGreatBest}} |
Best Great League entry |
{{pvpUltraBest}} |
Best Ultra League entry |
{{userHasPvpTracks}} |
Whether the user has PVP tracking active |
{{pvpUserRanking}} |
User's configured max PVP rank to display |
Each PVP entry (inside {{#each pvpGreat}} etc.) exposes:
| Field | Description |
|---|---|
this.fullName |
Full name including form and mega |
this.fullNameEng |
English version |
this.rank |
PVP rank |
this.cp |
CP at that rank |
this.level |
Level at that rank |
this.cap |
Level cap (e.g. 50 or 51) |
this.levelWithCap |
Combined level/cap display |
this.percentage |
Stat product percentage |
this.evolution |
true if mega evolution |
this.baseStats |
Base stats (for calculateCp) |
this.name / this.nameEng |
Pokemon name |
this.monsterName |
Just the pokemon name |
this.formName |
Just the form name |
PVP example:
{{#each pvpGreat}}{{#if this.rank}}{{#compare this.rank '<=' ../pvpUserRanking}}
Great League: {{this.fullName}} #{{this.rank}} @{{this.cp}}CP (Lvl. {{this.levelWithCap}})
{{/compare}}{{/if}}{{/each}}
Monster change variables (monsterchange / monsterchangeNoIv):
In addition to the standard monster variables for the new spawn, these are available:
| Variable | Description |
|---|---|
{{oldFullName}} |
Previous pokemon's full name |
{{oldIv}} |
Previous pokemon's IV |
{{oldIvKnown}} |
Whether the old IV was known |
{{oldCp}} |
Previous pokemon's CP |
Raid (raid)¶
| Variable | Description |
|---|---|
{{pokemonId}} / {{id}} |
Pokemon ID |
{{name}} |
Boss name (localised) |
{{fullName}} |
Boss name with form |
{{{gymName}}} |
Gym name |
{{level}} |
Raid level / tier |
{{time}} |
Raid end time |
{{tthh}} |
Hours until raid ends |
{{tthm}} |
Minutes until raid ends |
{{tths}} |
Seconds until raid ends |
{{end}} |
Unix timestamp of raid end |
{{hatchTime}} |
Hatch time |
{{formId}} |
Form ID |
{{formName}} |
Form name |
{{cp}} |
Raid boss CP |
{{quickMoveName}} |
Quick move name |
{{chargeMoveName}} |
Charge move name |
{{quickMoveEmoji}} |
Quick move type emoji |
{{chargeMoveEmoji}} |
Charge move type emoji |
{{teamName}} |
Gym's controlling team |
{{description}} |
Gym description |
{{{gymUrl}}} |
Gym image URL |
{{gymColor}} |
Embed colour for gym team |
{{ex}} |
EX-eligible gym (truthy/falsy) |
{{genderData.emoji}} |
Boss gender emoji |
{{shinyPossibleEmoji}} |
Sparkle if shiny possible |
{{baseStats}} |
Base stats object (for calculateCp) |
{{{weaknessEmoji}}} |
Type weakness emojis |
{{boostingWeathersEmoji}} |
Weather boost emojis |
{{{imgUrl}}} |
Boss image URL |
Catch CP calculation:
Normal: {{calculateCp baseStats 20 10 10 10}}-{{calculateCp baseStats 20 15 15 15}}
Boosted: {{calculateCp baseStats 25 10 10 10}}-{{calculateCp baseStats 25 15 15 15}}
RSVP data (if enabled):
{{#if rsvps}}{{#each rsvps}}
{{time}} <t:{{timeSlot}}:R> Going: {{goingCount}} Maybe: {{maybeCount}}
{{/each}}{{/if}}
Egg (egg)¶
| Variable | Description |
|---|---|
{{{gymName}}} |
Gym name |
{{level}} |
Egg level / tier |
{{levelName}} |
Level name (e.g. "Mega Raid", "5-Star Raid") |
{{time}} |
Raid end time |
{{hatchTime}} |
Hatch time |
{{tthh}} |
Hours until hatch |
{{tthm}} |
Minutes until hatch |
{{tths}} |
Seconds until hatch |
{{teamName}} |
Gym's controlling team |
{{teamEmoji}} |
Team emoji |
{{description}} |
Gym description |
{{gymColor}} |
Embed colour for gym team |
{{ex}} |
EX-eligible gym |
{{{gymUrl}}} |
Gym image URL |
{{{imgUrl}}} |
Egg image URL |
Quest (quest)¶
| Variable | Description |
|---|---|
{{{pokestopName}}} |
Pokestop name |
{{pokestopUrl}} |
Pokestop image URL |
{{{questString}}} |
Quest task description (e.g. "Battle in 3 raids") |
{{{rewardString}}} |
Reward description |
{{monsterNames}} |
Reward pokemon names (if applicable) |
{{itemNames}} |
Reward item names (if applicable) |
{{dustAmount}} |
Stardust reward amount |
{{itemAmount}} |
Item reward quantity |
{{energyAmount}} |
Mega energy reward amount |
{{energyMonstersNames}} |
Pokemon for mega energy reward |
{{isShiny}} |
Whether the pokemon reward is shiny |
{{with_ar}} |
Whether AR scan is required before spinning |
{{{imgUrl}}} |
Reward image URL |
{{{stickerUrl}}} |
Reward sticker URL |
Invasion (invasion)¶
| Variable | Description |
|---|---|
{{{pokestopName}}} |
Pokestop name |
{{gruntType}} |
Invasion type (e.g. Rock, Dragon, Mixed) |
{{gruntTypeId}} |
Invasion type ID |
{{gruntTypeColor}} |
Colour for the grunt type |
{{gruntTypeEmoji}} |
Grunt type emoji |
{{gruntName}} |
Grunt name |
{{gruntRewards}} |
Formatted reward string |
{{gruntRewardsList}} |
Structured reward data (see below) |
{{genderData.name}} |
Grunt gender name |
{{genderData.emoji}} |
Grunt gender emoji |
{{gender}} |
Grunt gender ID |
{{displayTypeId}} |
Display type (<=6 = grunt, 7 = gold stop, 8 = Kecleon, 9 = showcase) |
{{time}} |
Invasion end time |
{{incidentExpiration}} |
Unix timestamp of expiration |
{{tthh}} |
Hours remaining |
{{tthm}} |
Minutes remaining |
{{tths}} |
Seconds remaining |
{{{imgUrl}}} |
Invasion image URL |
{{pokestopUrl}} |
Pokestop image URL |
Reward list structure:
{{#compare gruntRewardsList.first.chance '==' 100}}
{{#forEach gruntRewardsList.first.monsters}}{{this.name}}{{#unless isLast}}, {{/unless}}{{/forEach}}
{{/compare}}
{{#compare gruntRewardsList.first.chance '<' 100}}
{{gruntRewardsList.first.chance}}%: {{#forEach gruntRewardsList.first.monsters}}{{this.name}}{{#unless isLast}}, {{/unless}}{{/forEach}}
{{gruntRewardsList.second.chance}}%: {{#forEach gruntRewardsList.second.monsters}}{{this.name}}{{#unless isLast}}, {{/unless}}{{/forEach}}
{{/compare}}
Lure (lure)¶
| Variable | Description |
|---|---|
{{{pokestopName}}} |
Pokestop name |
{{lureTypeName}} |
Lure type (localised) |
{{lureTypeNameEng}} |
Lure type (English) |
{{lureTypeId}} |
Lure type ID |
{{lureTypeEmoji}} |
Lure type emoji |
{{lureTypeColor}} |
Lure colour |
{{time}} |
Lure end time |
{{tthh}} |
Hours remaining |
{{tthm}} |
Minutes remaining |
{{tths}} |
Seconds remaining |
{{{imgUrl}}} |
Lure image URL |
{{pokestopUrl}} |
Pokestop image URL |
Nest (nest)¶
| Variable | Description |
|---|---|
{{{nestName}}} |
Nest name |
{{pokemonId}} |
Pokemon ID |
{{name}} |
Pokemon name (localised) |
{{nameEng}} |
Pokemon name (English) |
{{pokemonSpawnAvg}} |
Average spawns per hour |
{{pokemonCount}} |
Total pokemon count |
{{resetDate}} |
Date nest last reset |
{{disappearDate}} |
Predicted nest end date |
{{color}} |
Embed colour (type-based) |
{{emojiString}} |
Type emoji |
{{shinyPossibleEmoji}} |
Sparkle if shiny possible |
{{boostingWeathersEmoji}} |
Weather boost emojis |
{{{imgUrl}}} |
Pokemon image URL |
Gym (gym)¶
| Variable | Description |
|---|---|
{{{gymName}}} |
Gym name |
{{teamName}} |
Controlling team name |
{{color}} |
Team colour |
{{slotsAvailable}} |
Number of open slots |
{{inBattle}} |
Whether the gym is under attack |
{{{gymUrl}}} |
Gym image URL |
Weather Change (weatherchange)¶
| Variable | Description |
|---|---|
{{weatherName}} |
New weather name |
{{weatherEmoji}} |
New weather emoji |
{{oldWeatherName}} |
Previous weather name |
{{oldWeatherEmoji}} |
Previous weather emoji |
{{weatherCurrent}} |
Current weather ID |
{{weatherCurrentName}} |
Current weather name |
{{weatherCurrentEmoji}} |
Current weather emoji |
{{weatherNext}} |
Forecast weather ID |
{{weatherNextName}} |
Forecast weather name |
{{weatherNextEmoji}} |
Forecast weather emoji |
{{weatherChangeTime}} |
Change time (HH:00) |
{{activePokemons}} |
Array of affected tracked pokemon |
Active pokemon iteration:
{{#each activePokemons}}
**{{this.name}}** {{#isnt this.formName 'Normal'}} {{this.formName}}{{/isnt}} - {{round this.iv}}% - {{this.cp}}CP
{{/each}}
Fort Update (fort-update)¶
| Variable | Description |
|---|---|
{{{name}}} |
Fort name |
{{fortType}} |
pokestop or gym |
{{isNew}} |
Whether this is a newly added fort |
{{isRemoval}} |
Whether this fort was removed |
{{isEdit}} |
Whether this is an edit to an existing fort |
{{isEditName}} |
Name was changed |
{{isEditDescription}} |
Description was changed |
{{isEditLocation}} |
Location was changed |
{{isEditImgUrl}} |
Image was changed |
{{{newName}}} |
New name (if changed) |
{{{oldName}}} |
Previous name (if changed) |
{{{newDescription}}} |
New description |
{{{oldDescription}}} |
Previous description |
{{oldLatitude}} |
Previous latitude (if moved) |
{{oldLongitude}} |
Previous longitude (if moved) |
{{{newImageUrl}}} |
New image URL |
{{{oldImageUrl}}} |
Previous image URL |
{{description}} |
Fort description |
{{{imgUrl}}} |
Fort image URL |
{{areas}} |
Matched area names |
Max Battle (maxbattle)¶
| Variable | Description |
|---|---|
{{fullName}} |
Pokemon name |
{{levelName}} |
Battle level name |
{{{stationName}}} |
Power Spot station name |
{{quickMoveName}} |
Quick move name |
{{chargeMoveName}} |
Charge move name |
{{time}} |
Battle end time |
{{tthm}} |
Minutes remaining |
{{tths}} |
Seconds remaining |
{{color}} |
Embed colour |
{{{imgUrl}}} |
Pokemon image URL |
Multiple template styles¶
You can define multiple templates for the same alert type by giving them different id values. Users select a template on their tracking commands with the template parameter:
Example -- a compact "hundo" template alongside the standard one:
[
{
"id": "standard",
"type": "monster",
"default": true,
"platform": "discord",
"template": {
"embed": {
"title": "{{fullName}} {{genderData.emoji}} {{round iv}}% ({{atk}}/{{def}}/{{sta}})",
"description": "CP {{cp}} | Lvl {{level}}\nDespawns: {{time}} ({{tthm}}m {{tths}}s)\n[Google]({{{googleMapUrl}}})",
"thumbnail": { "url": "{{{imgUrl}}}" },
"image": { "url": "{{{staticMap}}}" }
}
}
},
{
"id": "hundo",
"type": "monster",
"default": false,
"platform": "discord",
"template": {
"embed": {
"color": "#FFD700",
"title": "💯 HUNDO {{fullName}} 💯",
"description": "CP {{cp}} | Lvl {{level}}\n{{time}} ({{tthm}}m left)\n[Map]({{{googleMapUrl}}})",
"thumbnail": { "url": "{{{imgUrl}}}" }
}
}
}
]
Customising templates¶
Tips¶
- Start from the defaults. Copy
config/defaults/dts.jsontoconfig/dts.jsonand modify from there. - Use
@includefor long descriptions. Place shared template fragments inconfig/dts/as.txtfiles. - Test with the Discord embed visualiser. The Embed Visualizer helps you preview how your embed will look.
- Use triple braces for URLs. Always use
{{{url}}}for any field containing a URL to avoid HTML encoding. - Conditional blocks save space. Use
{{#if}}to only show fields when data exists (e.g. weather boost, PVP ranks).
Example: minimal Telegram monster template¶
{
"id": "standard",
"type": "monster",
"language": "en",
"default": true,
"platform": "telegram",
"template": {
"content": "[\u200A]({{{staticMap}}}) **{{fullName}}** {{genderData.emoji}} {{round iv}}% ({{atk}}/{{def}}/{{sta}})\nCP {{cp}} | Lvl {{level}}\nDespawns: {{time}} ({{tthm}}m {{tths}}s)\n[Google]({{{googleMapUrl}}}) | [Apple]({{{appleMapUrl}}})",
"parse_mode": "Markdown",
"sticker": "{{{stickerUrl}}}",
"location": true,
"webpage_preview": true
}
}