Skip to main content

Common Patterns

Practical, copy-paste-ready patterns for the most frequent CallMeLater use cases. Each example shows the SDK code alongside the equivalent curl request.


Trial Expiration

Schedule a webhook to fire when a user's trial ends. If they subscribe before the timer runs out, cancel it by idempotency key.

Schedule the expiration webhook:

import { CallMeLater } from 'callmelater';

const client = new CallMeLater({ apiToken: 'sk_live_...' });

const action = await client.http('https://api.example.com/subscriptions/expire')
.post()
.payload({ user_id: 123, action: 'downgrade_to_free' })
.inDays(14)
.idempotencyKey('trial:user:123')
.send();

console.log(action.id); // act_...

Cancel if the user subscribes:

await client.cancelAction({ idempotency_key: 'trial:user:123' });

Tips: Use max_attempts: 5 -- this webhook matters. Make your endpoint idempotent so duplicate deliveries are harmless.


Deployment Approval

Get human sign-off before deploying to production. The approval is sent immediately and the callback URL receives the response.

await client.reminder('Production deploy v2.4.1')
.to('tech-lead@example.com')
.toMany(['devops@example.com'])
.message('Approve deployment of v2.4.1 to production?')
.buttons('Approve', 'Reject')
.noSnooze()
.expiresInDays(1)
.escalateTo(['cto@example.com'], 2)
.inMinutes(0)
.callback('https://ci.example.com/webhooks/approval')
.idempotencyKey('deploy:v2.4.1:prod')
.send();

Handle the callback to continue (or abort) your CI pipeline based on the response:

// In your callback handler
app.post('/webhooks/approval', (req, res) => {
const { event, payload } = req.body;
if (event === 'reminder.responded' && payload.response === 'confirmed') {
triggerDeploy('v2.4.1', 'production');
}
res.sendStatus(200);
});

Delayed Cleanup

Delete temporary resources (exports, uploads, sandbox data) after a retention period. Schedule the cleanup when you create the resource.

await client.http('https://api.example.com/exports/exp_abc123')
.delete()
.inDays(30)
.idempotencyKey('cleanup:export:exp_abc123')
.noRetry()
.send();

Tips: Use noRetry() / max_attempts: 1 -- if the resource was already deleted manually, one failed attempt is enough. Include the resource ID in the idempotency key for easy cancellation.


Scheduled Reports

Generate a weekly report every Monday morning. When the webhook fires, re-schedule the next one from your callback handler.

await client.http('https://api.example.com/reports/generate')
.post()
.payload({ type: 'weekly_summary', week: '2026-W08' })
.at('next_monday')
.timezone('America/New_York')
.idempotencyKey('report:weekly:2026-W08')
.callback('https://api.example.com/callbacks/report-done')
.send();

Re-schedule from your callback:

// When the report webhook completes, schedule the next week
app.post('/callbacks/report-done', async (req, res) => {
if (req.body.event === 'action.executed') {
const nextWeek = getNextWeekString(); // e.g. "2026-W09"
await client.http('https://api.example.com/reports/generate')
.post()
.payload({ type: 'weekly_summary', week: nextWeek })
.at('next_monday')
.timezone('America/New_York')
.idempotencyKey(`report:weekly:${nextWeek}`)
.callback('https://api.example.com/callbacks/report-done')
.send();
}
res.sendStatus(200);
});

Tips: Include the week in the idempotency key to prevent duplicates. Set timezone so "Monday morning" stays consistent across DST changes.


Follow-Up Sequence

Send an onboarding email series at day 1, day 3, and day 7 after signup. Each email gets its own action with a unique idempotency key so you can cancel the remaining ones if the user unsubscribes.

const userId = 123;
const steps = [
{ days: 1, template: 'welcome', key: 'day1' },
{ days: 3, template: 'getting_started', key: 'day3' },
{ days: 7, template: 'pro_tips', key: 'day7' },
];

for (const step of steps) {
await client.http('https://api.example.com/emails/send')
.post()
.payload({ user_id: userId, template: step.template })
.inDays(step.days)
.idempotencyKey(`onboard:user:${userId}:${step.key}`)
.send();
}

Cancel remaining emails if the user unsubscribes:

// Cancel all pending onboarding emails for a user
for (const key of ['day1', 'day3', 'day7']) {
await client.cancelAction({ idempotency_key: `onboard:user:${userId}:${key}` });
}

Invoice Reminder with Escalation

Remind a customer about an upcoming payment. If they do not respond within 48 hours, escalate to your accounts receivable team.

await client.reminder('Invoice INV-2026-042 payment due')
.to('billing@customer.com')
.message(
'Invoice INV-2026-042 ($3,200) is due in 5 days. ' +
'Click Confirm to mark as scheduled for payment.'
)
.buttons('Confirm Payment', 'Decline')
.allowSnooze(2)
.expiresInDays(7)
.escalateTo(['ar@yourcompany.com', '+15551234567'], 48)
.at('2026-02-20T09:00:00')
.timezone('America/Chicago')
.idempotencyKey('invoice:INV-2026-042:reminder')
.callback('https://api.example.com/webhooks/invoice-response')
.send();

Tips: Schedule the reminder a few days before the due date using scheduled_for. Set escalation.after_hours based on urgency -- 48 hours gives the customer time to respond before your team gets involved. Include a phone number in escalation contacts for SMS notifications.