Link Expiration for Time-Sensitive Campaigns
Create urgency and automate campaign lifecycles with automatic link expiration. Perfect for flash sales, event registrations, limited-time offers, and temporary access.
What You'll Build
In this tutorial, you'll learn how to create self-expiring links that automatically redirect to a custom page after a deadline. We'll cover flash sales, event tickets, and temporary access scenarios.
Why Use Link Expiration?
Link expiration creates urgency and automates campaign management:
- Flash Sales - Automatically end sale promotions at midnight
- Event Registration - Close sign-ups when events reach capacity or start time
- Limited-Time Offers - Create FOMO with countdown timers
- Temporary Access - Grant time-limited access to resources
- Beta Testing - Automatically close beta programs after test period
- Early Bird Pricing - Switch to regular pricing automatically
Creating Your First Expiring Link
Flash Sale Example
Let's create a flash sale link that expires at midnight and redirects to a "sale ended" page:
const API_KEY = process.env.SNIPLINK_API_KEY;
const WORKSPACE_ID = 'your-workspace-id';
const API_BASE = 'https://api.sniplink.co/v1';
async function createFlashSaleLink(
saleUrl: string,
saleEndTime: Date
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: saleUrl,
title: '24-Hour Flash Sale - 50% Off',
expiresAt: saleEndTime.toISOString(),
redirects: {
expired: 'https://yourstore.com/sale-ended',
},
tags: ['flash-sale', 'limited-time'],
}),
}
);
const { data } = await response.json();
return data.link;
}
// Create a flash sale that ends at midnight
const midnight = new Date();
midnight.setHours(23, 59, 59, 999);
const saleLink = await createFlashSaleLink(
'https://yourstore.com/flash-sale',
midnight
);
console.log(`Flash sale link: ${saleLink.shortUrl}`);
console.log(`Expires at: ${saleLink.expiresAt}`);
// After midnight, automatically redirects to /sale-endedTimezone Note: Always use ISO 8601 format with explicit timezone (like 2025-12-31T23:59:59Z for UTC) to avoid timezone confusion. Consider your audience's timezone when setting expiration times.
Custom Expired Link Pages
Instead of showing a 404 error, create branded "expired" landing pages that maintain the user experience:
Multiple Redirect Strategies
async function createEventRegistrationLink(
eventUrl: string,
registrationDeadline: Date,
eventStartTime: Date
) {
// Strategy 1: Redirect to "Registration Closed" page
if (new Date() < eventStartTime) {
return await fetch(`${API_BASE}/workspaces/${WORKSPACE_ID}/links`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: eventUrl,
title: 'Tech Conference 2025 - Registration',
expiresAt: registrationDeadline.toISOString(),
redirects: {
expired: 'https://yoursite.com/registration-closed',
},
}),
}).then(r => r.json());
}
// Strategy 2: Redirect to event recap/highlights after event ends
return await fetch(`${API_BASE}/workspaces/${WORKSPACE_ID}/links`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: eventUrl,
title: 'Tech Conference 2025 - Event Recap',
expiresAt: eventStartTime.toISOString(),
redirects: {
expired: 'https://yoursite.com/event-highlights',
},
}),
}).then(r => r.json());
}
// Usage
const registrationDeadline = new Date('2025-06-15T23:59:59Z');
const eventStart = new Date('2025-06-20T09:00:00Z');
const eventLink = await createEventRegistrationLink(
'https://yoursite.com/conference/register',
registrationDeadline,
eventStart
);Creating Effective Expired Pages
Your expired redirect page should:
- Acknowledge the expiration - "This offer has ended"
- Provide context - Explain what the link was for
- Offer alternatives - Direct to current offers or newsletter signup
- Maintain branding - Keep the user in your ecosystem
Scheduling Future Activation
Use publishAt to create links that don't become active until a specific time. This is perfect for coordinated launches:
async function createScheduledProductLaunch(
productUrl: string,
launchDate: Date,
saleEndDate: Date
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: productUrl,
title: 'New Product Launch - Early Access',
publishAt: launchDate.toISOString(),
expiresAt: saleEndDate.toISOString(),
redirects: {
notYetAvailable: 'https://yoursite.com/coming-soon',
expired: 'https://yoursite.com/launch-ended',
},
tags: ['product-launch', 'early-access'],
}),
}
);
const { data } = await response.json();
return data.link;
}
// Schedule launch for Black Friday at midnight, end at Sunday midnight
const blackFriday = new Date('2025-11-29T00:00:00Z');
const cyberMonday = new Date('2025-12-01T23:59:59Z');
const launchLink = await createScheduledProductLaunch(
'https://yourstore.com/black-friday-deals',
blackFriday,
cyberMonday
);
// Distribute the link in advance
// Before Nov 29: redirects to /coming-soon
// Nov 29-Dec 1: redirects to /black-friday-deals
// After Dec 1: redirects to /launch-endedCombining with Other Features
Expiration + Click Limits
Create scarcity with both time limits and capacity limits:
async function createLimitedTicketLink(
ticketUrl: string,
saleEndDate: Date,
maxTickets: number
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: ticketUrl,
title: 'Concert Tickets - Limited Availability',
expiresAt: saleEndDate.toISOString(),
maxClicks: maxTickets,
redirects: {
expired: 'https://yoursite.com/tickets-sold-out',
maxClicksReached: 'https://yoursite.com/tickets-sold-out',
},
tags: ['tickets', 'limited-edition'],
}),
}
);
return (await response.json()).data.link;
}
// 100 tickets OR sales end in 7 days, whichever comes first
const ticketLink = await createLimitedTicketLink(
'https://yoursite.com/concert/checkout',
new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
100
);Expiration + Password Protection
Create time-limited exclusive access:
async function createVIPEarlyAccess(
exclusiveUrl: string,
earlyAccessEnd: Date,
vipPassword: string
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: exclusiveUrl,
title: 'VIP Early Access - Premium Members Only',
password: vipPassword,
expiresAt: earlyAccessEnd.toISOString(),
redirects: {
expired: 'https://yoursite.com/now-public',
},
tags: ['vip', 'early-access'],
}),
}
);
return (await response.json()).data.link;
}
// VIP members get 48-hour early access with password
// After expiration, content becomes public (no password needed)
const earlyAccessEnd = new Date(Date.now() + 48 * 60 * 60 * 1000);
const vipLink = await createVIPEarlyAccess(
'https://yoursite.com/premium-content',
earlyAccessEnd,
'VIP2025'
);Monitoring and Managing Expiring Links
Find Links Expiring Soon
async function getExpiringLinks(daysAhead: number = 7) {
// Get all links in workspace
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links?limit=100`,
{
headers: { 'X-API-Key': API_KEY },
}
);
const { data } = await response.json();
const now = new Date();
const futureDate = new Date(now.getTime() + daysAhead * 24 * 60 * 60 * 1000);
// Filter links expiring in the next N days
return data.links.filter((link: any) => {
if (!link.expiresAt) return false;
const expiresAt = new Date(link.expiresAt);
return expiresAt > now && expiresAt <= futureDate;
});
}
// Get links expiring in the next 3 days
const expiringLinks = await getExpiringLinks(3);
console.log(`${expiringLinks.length} links expiring soon:`);
expiringLinks.forEach((link: any) => {
console.log(`- ${link.title}: ${link.expiresAt}`);
});Extend Expiration Date
Update the expiration date if you need to extend a campaign:
async function extendLinkExpiration(
linkId: string,
newExpirationDate: Date
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
expiresAt: newExpirationDate.toISOString(),
}),
}
);
return (await response.json()).data.link;
}
// Extend flash sale by 24 hours
const tomorrow = new Date(Date.now() + 24 * 60 * 60 * 1000);
const extendedLink = await extendLinkExpiration(
'link-id-here',
tomorrow
);
console.log(`Extended to: ${extendedLink.expiresAt}`);Remove Expiration
Make a temporary link permanent by removing the expiration:
async function removeExpiration(linkId: string) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
expiresAt: null, // Remove expiration
}),
}
);
return (await response.json()).data.link;
}
// Convert limited-time beta to permanent access
await removeExpiration('beta-link-id');Real-World Use Cases
1. Early Bird Conference Tickets
async function createEarlyBirdTickets() {
// Phase 1: Super Early Bird (30 days, 50 tickets)
const superEarlyBird = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://conference.com/register?tier=super-early',
title: 'Super Early Bird - $299',
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
maxClicks: 50,
redirects: {
expired: 'https://conference.com/early-bird',
maxClicksReached: 'https://conference.com/early-bird',
},
}),
}
).then(r => r.json());
// Phase 2: Early Bird (60 days, 100 tickets)
const earlyBird = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://conference.com/register?tier=early',
title: 'Early Bird - $399',
publishAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
expiresAt: new Date(Date.now() + 60 * 24 * 60 * 60 * 1000).toISOString(),
maxClicks: 100,
redirects: {
notYetAvailable: 'https://conference.com/super-early-bird',
expired: 'https://conference.com/regular-price',
maxClicksReached: 'https://conference.com/regular-price',
},
}),
}
).then(r => r.json());
return { superEarlyBird, earlyBird };
}2. Course Enrollment Deadlines
async function createCourseEnrollmentLink(
courseUrl: string,
enrollmentDeadline: Date,
courseStartDate: Date
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: courseUrl,
title: 'Web Development Bootcamp - Spring 2025',
expiresAt: enrollmentDeadline.toISOString(),
redirects: {
expired: `https://courses.com/enrollment-closed?next=${courseStartDate.toISOString()}`,
},
tags: ['course', 'spring-2025'],
}),
}
);
return (await response.json()).data.link;
}
// Close enrollment 1 week before course starts
const courseStart = new Date('2025-03-15T09:00:00Z');
const enrollmentDeadline = new Date(courseStart.getTime() - 7 * 24 * 60 * 60 * 1000);
const enrollmentLink = await createCourseEnrollmentLink(
'https://courses.com/bootcamp/enroll',
enrollmentDeadline,
courseStart
);3. Job Application Deadlines
async function createJobPostingLink(
jobUrl: string,
applicationDeadline: Date
) {
const response = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: jobUrl,
title: 'Senior Developer Position - Applications Open',
expiresAt: applicationDeadline.toISOString(),
redirects: {
expired: 'https://careers.com/position-filled',
},
tags: ['hiring', 'senior-developer'],
}),
}
);
return (await response.json()).data.link;
}
// Close applications in 2 weeks
const deadline = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000);
const jobLink = await createJobPostingLink(
'https://careers.com/senior-dev/apply',
deadline
);Best Practices
1. Timezone Considerations
- Use UTC - Store all dates in UTC to avoid timezone confusion
- Display local time - Show users deadlines in their local timezone
- Business hours - End sales during business hours, not at midnight in an arbitrary timezone
// Bad: Hardcoded to your local timezone
const midnight = new Date('2025-12-31T23:59:59'); // Which timezone?
// Good: Explicit UTC
const midnightUTC = new Date('2025-12-31T23:59:59Z');
// Better: Specific timezone for your audience
const midnightEST = new Date('2025-12-31T23:59:59-05:00');
// Best: Calculate based on user's timezone
function getEndOfDay(date: Date, timezoneOffset: number) {
const utcDate = new Date(date);
utcDate.setHours(23 - timezoneOffset, 59, 59, 999);
return utcDate;
}2. Buffer Time
Add buffer time to avoid edge cases:
// Instead of expiring exactly at event start
const eventStart = new Date('2025-06-20T09:00:00Z');
// Expire registration 1 hour before
const registrationDeadline = new Date(
eventStart.getTime() - 60 * 60 * 1000
);
// This gives you time to prepare attendee lists, etc.3. Clear Communication
Make deadlines visible and create urgency:
- Include countdown timers on landing pages
- Send reminder emails at 24h, 6h, 1h before expiration
- Show "Ends in X hours" in email subject lines
- Display expiration date clearly on destination page
4. Analytics and Monitoring
Track performance of time-sensitive campaigns:
async function monitorCampaignPerformance(linkId: string) {
const link = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{ headers: { 'X-API-Key': API_KEY } }
).then(r => r.json());
const analytics = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}/analytics`,
{ headers: { 'X-API-Key': API_KEY } }
).then(r => r.json());
const expiresAt = new Date(link.data.link.expiresAt);
const timeRemaining = expiresAt.getTime() - Date.now();
const hoursRemaining = timeRemaining / (1000 * 60 * 60);
console.log(`Campaign Status:`);
console.log(`- Hours remaining: ${hoursRemaining.toFixed(1)}`);
console.log(`- Total clicks: ${analytics.data.totalClicks}`);
console.log(`- Clicks per hour: ${(analytics.data.totalClicks / (48 - hoursRemaining)).toFixed(1)}`);
// Decision: Extend if performing well
if (analytics.data.totalClicks > 1000 && hoursRemaining < 12) {
console.log('⚠️ Consider extending this high-performing campaign');
}
}5. Test Before Launch
Always test expiration behavior:
// Create a test link that expires in 2 minutes
async function testExpirationBehavior() {
const testLink = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://yoursite.com/test-destination',
title: 'TEST - Expires in 2 minutes',
expiresAt: new Date(Date.now() + 2 * 60 * 1000).toISOString(),
redirects: {
expired: 'https://yoursite.com/test-expired',
},
tags: ['test'],
}),
}
).then(r => r.json());
console.log(`Test link: ${testLink.data.link.shortUrl}`);
console.log('Test before expiration, then wait 2 minutes and test again');
}Automation Ideas
Scheduled Campaign Creator
Automate creation of recurring campaigns:
async function createWeeklyFlashSale() {
// Every Friday at 9 AM, create a 24-hour flash sale
const now = new Date();
const nextFriday = new Date(now);
nextFriday.setDate(now.getDate() + ((5 - now.getDay() + 7) % 7));
nextFriday.setHours(9, 0, 0, 0);
const saleEnd = new Date(nextFriday.getTime() + 24 * 60 * 60 * 1000);
const flashSale = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links`,
{
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://shop.com/flash-sale',
title: `Flash Sale - ${nextFriday.toLocaleDateString()}`,
publishAt: nextFriday.toISOString(),
expiresAt: saleEnd.toISOString(),
redirects: {
notYetAvailable: 'https://shop.com/coming-soon',
expired: 'https://shop.com/missed-it',
},
tags: ['flash-sale', 'weekly'],
}),
}
).then(r => r.json());
return flashSale.data.link;
}
// Run this as a cron job every Monday to create Friday's sale
// crontab: 0 9 * * 1 /path/to/create-weekly-sale.shExpiration Reminder System
async function sendExpirationReminders() {
const expiringLinks = await getExpiringLinks(1); // 24 hours
for (const link of expiringLinks) {
const analytics = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${link.id}/analytics`,
{ headers: { 'X-API-Key': API_KEY } }
).then(r => r.json());
// Send email/SMS to users who clicked but didn't convert
await sendReminder({
subject: `Last chance! ${link.title} expires in 24 hours`,
clicks: analytics.data.totalClicks,
expiresAt: link.expiresAt,
link: link.shortUrl,
});
}
}
// Run hourly: 0 * * * * /path/to/send-reminders.shTroubleshooting
Link Expired Too Early
Problem: Link expired before the specified time.
Solution: Check timezone. Use .toISOString() to ensure UTC format.
Expired Redirect Not Working
Problem: After expiration, users see error instead of custom page.
Solution: Verify redirects.expired is set and URL is valid:
// Check link configuration
const link = await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{ headers: { 'X-API-Key': API_KEY } }
).then(r => r.json());
console.log(link.data.link.redirects);
// Should show: { expired: "https://..." }Can't Update Expired Link
Problem: Need to extend expiration but link is already expired.
Solution: You can still update expired links via API:
// Even if link is expired, you can extend it
await fetch(
`${API_BASE}/workspaces/${WORKSPACE_ID}/links/${linkId}`,
{
method: 'PATCH',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
}),
}
);Next Steps
Password Protection →
Combine expiration with password protection for time-limited exclusive access
QR Code Generator →
Create expiring QR codes for event tickets and limited-time print campaigns
Links API Reference →
Complete API documentation for expiresAt, publishAt, and redirect configuration
Analytics Dashboard →
Track performance of time-sensitive campaigns with real-time analytics
Questions or Feedback?
Need help with link expiration strategies? Loading contact information or check out our comprehensive guides.