Reference
Card template variables
A creator's card design stores its text as a template, so {Token} placeholders arrive in cardDesign.textLine1.content and textLine2.content unresolved. These 10 tokens are the complete set, and each row names the payload field you resolve it from yourself.
Velora already substitutes these.On our own overlays the tokens are replaced before render. You only need this page if you are building your own overlay and rendering a creator's cardDesign yourself — then the raw template text reaches you with the tokens intact, and you substitute from the payload fields below.
The variables
Matching is case-insensitive — {times}, {Times} and {TIMES}are the same token. Tokens that don't apply to a given event resolve to an empty string; unknown tokens are left exactly as written rather than silently removed.
| Token | Meaning | Payload field | Example |
|---|---|---|---|
{User}also {Name}, {DisplayName}, {Viewer} | Display name | displayName | CompTech |
{Username}also {Handle} | Username (@handle) | username | comptech |
{Reward} | Reward name | rewardTitle / itemName | Daily MoonPie |
{Times} | Redeem count (this user, this reward) | counts.lifetime | 9 |
{Place} | Ordinal place (1st / 2nd) | builtInType | 1st |
{Amount} | Volts amount | amount | 500 |
{Months} | Subscription months | months | 3 |
{Tier} | Subscription tier | tier | 2 |
{Viewers} | Raid viewer count | viewers | 42 |
{Message} | Viewer's attached message | userMessage | hi chat! |
Where cardDesign comes from
cardDesign is the creator's saved design from the Channel Points card designer. It is stored on the reward itself, so the same design arrives with every redemption of that reward until the creator edits it. Two different events carry one:
channel.channel_points_redemptionA viewer redeemed a channel-points reward. Carries counts.lifetime and builtInType, which is what {Times} and {Place} resolve from.
channel.stream_alertFollow, sub, gift, resub, raid or Volts alert. Carries cardDesign plus a templateDataobject holding that alert type's own values (amount, months, tier, viewers, …).
A reward with no saved design sends alertMode: "default" and no cardDesign — render your own default rather than assuming the field is present.
Substituting it yourself
Do the whole template in one pass, not a chain of individual replacements. A chain re-scans text it has already substituted, so a viewer whose message literally contains {Reward} would have it expanded by a later step.
const VALUES = {
user: e.displayName || e.username,
username: e.username,
reward: e.rewardName,
times: String(e.counts?.lifetime ?? 1),
place: e.builtInType === 'first' ? '1st'
: e.builtInType === 'second' ? '2nd' : '',
message: e.userMessage || '',
};
const render = (tpl) =>
tpl.replace(/\{([A-Za-z]+)\}/g, (whole, token) => {
const v = VALUES[token.toLowerCase()];
return v !== undefined ? v : whole; // keep unknown tokens visible
});
render(cardDesign.textLine2.content);Leaving unknown tokens intact is deliberate — it surfaces a creator's typo instead of hiding it behind a blank space.
{Times} — the redeem count
counts.lifetime is the number of fulfilled redemptions of this reward by this viewer, all time, including the one being delivered. So a viewer's ninth redeem arrives as counts.lifetime: 9.
The field is omitted rather than sent as 0when we have no count — that lets you tell "no data" apart from "zero". If it is absent, render 1: the redemption in front of you is at least the first one.
// channel.channel_points_redemption (abridged)
{
"userId": "…",
"username": "comptech",
"displayName": "CompTech",
"rewardName": "Daily MoonPie",
"rewardCost": 500,
"userMessage": "hi chat!",
"counts": { "lifetime": 9 }, // → {Times}
"builtInType": null // "first" | "second" → {Place}
}