diff --git a/README.md b/README.md
index 5a338cf..7101eeb 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,10 @@ Easily manage and trigger webhooks directly from your browser! Compatible with F
**๐งช Test a Webhook:**
- When adding or editing a webhook, click the 'Test' button to send a test payload to your URL.
+**๐ DateTime Variables in Custom Payload:**
+- You can use `{{now.*}}` placeholders in custom payload JSON, including UTC values like `{{now.iso}}`, `{{now.date}}`, `{{now.time}}`, `{{now.unix}}`, and local values like `{{now.local.iso}}`.
+- Existing `{{triggeredAt}}` remains available as an alias for `{{now.iso}}` for backward compatibility.
+
**๐๏ธ Organize into Groups:**
- Use the group management dialog to add, delete, rename, or reorder groups via drag-and-drop.
diff --git a/options/options.html b/options/options.html
index 79ce0e4..4c0f252 100644
--- a/options/options.html
+++ b/options/options.html
@@ -131,12 +131,27 @@
__MSG_optionsAddWebhookHeader__
{{tab.audible}} - Is tab playing audio
{{tab.incognito}} - Is tab in incognito mode
{{tab.status}} - Tab loading status
- {{platform.arch}} - Plaform architecture
+ {{platform.arch}} - Platform architecture
{{platform.os}} - Operating system
{{platform.version}} - Platform version
{{browser}} - Browser information
{{platform}} - Platform information
- {{triggeredAt}} - Timestamp when triggered
+ {{now.iso}} - Current UTC time as ISO 8601
+ {{now.date}} - Current UTC date (YYYY-MM-DD)
+ {{now.time}} - Current UTC time (HH:mm:ss)
+ {{now.unix}} - Current UTC Unix timestamp in seconds
+ {{now.unix_ms}} - Current UTC Unix timestamp in milliseconds
+ {{now.year}} - Current UTC year
+ {{now.month}} - Current UTC month (1-12)
+ {{now.day}} - Current UTC day of month (1-31)
+ {{now.hour}} - Current UTC hour (0-23)
+ {{now.minute}} - Current UTC minute (0-59)
+ {{now.second}} - Current UTC second (0-59)
+ {{now.millisecond}} - Current UTC millisecond (0-999)
+ {{now.local.iso}} - Current local time as ISO 8601
+ {{now.local.date}} - Current local date (YYYY-MM-DD)
+ {{now.local.time}} - Current local time (HH:mm:ss)
+ {{triggeredAt}} - Alias for {{now.iso}}
{{identifier}} - Custom identifier
diff --git a/options/options.js b/options/options.js
index 68c5bf6..40029a2 100644
--- a/options/options.js
+++ b/options/options.js
@@ -663,6 +663,10 @@ const availableVariables = [
"{{tab.index}}", "{{tab.pinned}}", "{{tab.audible}}", "{{tab.incognito}}",
"{{tab.status}}", "{{browser}}", "{{platform}}", "{{triggeredAt}}", "{{identifier}}",
"{{platform.arch}}", "{{platform.os}}", "{{platform.version}}",
+ "{{now.iso}}", "{{now.date}}", "{{now.time}}", "{{now.unix}}", "{{now.unix_ms}}",
+ "{{now.year}}", "{{now.month}}", "{{now.day}}", "{{now.hour}}", "{{now.minute}}",
+ "{{now.second}}", "{{now.millisecond}}", "{{now.local.iso}}", "{{now.local.date}}",
+ "{{now.local.time}}",
];
// Implement autocompletion for custom payload
diff --git a/tests/sendWebhook.test.js b/tests/sendWebhook.test.js
index f788d50..622e22e 100644
--- a/tests/sendWebhook.test.js
+++ b/tests/sendWebhook.test.js
@@ -50,6 +50,7 @@ describe('sendWebhook', () => {
global.fetch = originalFetch;
global.browser = originalBrowser;
console.error = originalConsoleError;
+ jest.useRealTimers();
jest.clearAllMocks();
});
@@ -174,4 +175,51 @@ describe('sendWebhook', () => {
await expect(sendWebhook(webhook, false))
.rejects.toThrow('popupErrorCustomPayloadJsonParseError');
});
+
+ test('should replace all new now datetime placeholders', async () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2025-08-07T10:20:30.123Z'));
+
+ const webhook = {
+ url: 'https://custom.com',
+ customPayload: '{"iso":"{{now.iso}}","date":"{{now.date}}","time":"{{now.time}}","unix":{{now.unix}},"unixMs":{{now.unix_ms}},"year":{{now.year}},"month":{{now.month}},"day":{{now.day}},"hour":{{now.hour}},"minute":{{now.minute}},"second":{{now.second}},"millisecond":{{now.millisecond}},"localIso":"{{now.local.iso}}","localDate":"{{now.local.date}}","localTime":"{{now.local.time}}"}'
+ };
+
+ await sendWebhook(webhook, false);
+
+ const fetchBody = JSON.parse(global.fetch.mock.calls[0][1].body);
+ expect(fetchBody.iso).toBe('2025-08-07T10:20:30.123Z');
+ expect(fetchBody.date).toBe('2025-08-07');
+ expect(fetchBody.time).toBe('10:20:30');
+ expect(fetchBody.unix).toBe(1754562030);
+ expect(fetchBody.unixMs).toBe(1754562030123);
+ expect(fetchBody.year).toBe(2025);
+ expect(fetchBody.month).toBe(8);
+ expect(fetchBody.day).toBe(7);
+ expect(fetchBody.hour).toBe(10);
+ expect(fetchBody.minute).toBe(20);
+ expect(fetchBody.second).toBe(30);
+ expect(fetchBody.millisecond).toBe(123);
+ expect(fetchBody.localDate).toMatch(/^\d{4}-\d{2}-\d{2}$/);
+ expect(fetchBody.localTime).toMatch(/^\d{2}:\d{2}:\d{2}$/);
+ expect(fetchBody.localIso).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{2}:\d{2}$/);
+
+ });
+
+ test('should keep triggeredAt as alias for now.iso', async () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date('2025-08-07T10:20:30.123Z'));
+
+ const webhook = {
+ url: 'https://custom.com',
+ customPayload: '{"now":"{{now.iso}}","legacy":"{{triggeredAt}}"}'
+ };
+
+ await sendWebhook(webhook, false);
+
+ const fetchBody = JSON.parse(global.fetch.mock.calls[0][1].body);
+ expect(fetchBody.now).toBe('2025-08-07T10:20:30.123Z');
+ expect(fetchBody.legacy).toBe('2025-08-07T10:20:30.123Z');
+
+ });
});
diff --git a/utils/utils.js b/utils/utils.js
index 61c24d6..64667b6 100644
--- a/utils/utils.js
+++ b/utils/utils.js
@@ -83,6 +83,52 @@ function replaceI18nPlaceholders() {
}
}
+const padDatePart = (value, length = 2) => String(value).padStart(length, '0');
+
+const toLocalIsoString = (date) => {
+ const year = date.getFullYear();
+ const month = padDatePart(date.getMonth() + 1);
+ const day = padDatePart(date.getDate());
+ const hour = padDatePart(date.getHours());
+ const minute = padDatePart(date.getMinutes());
+ const second = padDatePart(date.getSeconds());
+ const millisecond = padDatePart(date.getMilliseconds(), 3);
+ const offsetMinutes = -date.getTimezoneOffset();
+ const sign = offsetMinutes >= 0 ? '+' : '-';
+ const absoluteOffset = Math.abs(offsetMinutes);
+ const offsetHour = padDatePart(Math.floor(absoluteOffset / 60));
+ const offsetMinute = padDatePart(absoluteOffset % 60);
+
+ return `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}${sign}${offsetHour}:${offsetMinute}`;
+};
+
+const buildDateTimeVariables = (date = new Date()) => {
+ const nowIso = date.toISOString();
+ const timestampMs = date.getTime();
+
+ return {
+ nowIso,
+ values: {
+ "{{now.iso}}": nowIso,
+ "{{now.date}}": nowIso.slice(0, 10),
+ "{{now.time}}": nowIso.slice(11, 19),
+ "{{now.unix}}": Math.floor(timestampMs / 1000),
+ "{{now.unix_ms}}": timestampMs,
+ "{{now.year}}": date.getUTCFullYear(),
+ "{{now.month}}": date.getUTCMonth() + 1,
+ "{{now.day}}": date.getUTCDate(),
+ "{{now.hour}}": date.getUTCHours(),
+ "{{now.minute}}": date.getUTCMinutes(),
+ "{{now.second}}": date.getUTCSeconds(),
+ "{{now.millisecond}}": date.getUTCMilliseconds(),
+ "{{now.local.iso}}": toLocalIsoString(date),
+ "{{now.local.date}}": `${date.getFullYear()}-${padDatePart(date.getMonth() + 1)}-${padDatePart(date.getDate())}`,
+ "{{now.local.time}}": `${padDatePart(date.getHours())}:${padDatePart(date.getMinutes())}:${padDatePart(date.getSeconds())}`,
+ "{{triggeredAt}}": nowIso,
+ },
+ };
+};
+
async function sendWebhook(webhook, isTest = false) {
const browserAPI = getBrowserAPI();
let selectors = Array.isArray(webhook?.selectors) ? [...webhook.selectors] : [];
@@ -91,11 +137,13 @@ async function sendWebhook(webhook, isTest = false) {
try {
let payload;
+ const dateTimeVariables = buildDateTimeVariables();
+
if (isTest) {
payload = {
url: "https://example.com",
test: true,
- triggeredAt: new Date().toISOString(),
+ triggeredAt: dateTimeVariables.nowIso,
};
} else {
// Get info about the active tab
@@ -185,7 +233,7 @@ async function sendWebhook(webhook, isTest = false) {
},
browser: browserInfo,
platform: platformInfo,
- triggeredAt: new Date().toISOString(),
+ triggeredAt: dateTimeVariables.nowIso,
};
if (webhook && webhook.identifier) {
@@ -212,9 +260,9 @@ async function sendWebhook(webhook, isTest = false) {
"{{platform.arch}}": platformInfo.arch || "unknown",
"{{platform.os}}": platformInfo.os || "unknown",
"{{platform.version}}": platformInfo.version,
- "{{triggeredAt}}": new Date().toISOString(),
"{{identifier}}": webhook.identifier || "",
- "{{selectorContent}}": selectorContent
+ "{{selectorContent}}": selectorContent,
+ ...dateTimeVariables.values,
};
let customPayloadStr = webhook.customPayload;