Build a trial reminder that stops when the customer upgrades
Keep trial reminders useful with a stored job ID, cancellation on upgrade and a final state check before sending.
A trial reminder has a short useful life. It should help someone who still has a trial to finish, and become obsolete when that person upgrades. The product rule belongs in your application; a scheduler only decides when to call it.
Store an intention, not a finished message
Create a reminder record with a stable trial identifier and the time at which the reminder would be useful. Keep email addresses and the message itself out of the scheduling payload when the receiver can look them up later. A small payload limits the amount of stale information that can travel through the system.
Schedule the HTTP request from your server and store the returned id on the reminder record. Use an idempotency key derived from the particular trial and reminder version, rather than the customer alone: the same customer might begin another trial or need a different reminder later.
Keep the external call recoverable
The following illustrative example schedules a request one day from execution. In an application, calculate the intended time from the trial's actual end date and your reminder policy.
Illustrative server-side example: this schedules delivery 24 hours from execution. Replace the environment variables and example record IDs, persist the returned job ID, and implement your own authenticated HTTPS receiver. The request follows the current API reference.
const apiKey = process.env.WEBHOOK_SCHEDULER_API_KEY;
const receiverUrl = process.env.REMINDER_WEBHOOK_URL;
if (!apiKey || !receiverUrl) throw new Error("Set the server-side API key and public HTTPS receiver URL.");
const response = await fetch("https://webhookscheduler.com/api/v1/schedule", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify({
url: receiverUrl,
method: "POST",
runAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
reference: "trial:example-123",
idempotencyKey: "trial-example-123-reminder-v1",
body: { resourceId: "example-123" },
}),
});
if (!response.ok) throw new Error(`Scheduling failed: ${response.status}`);
const job = await response.json();
if (!job.id) throw new Error("Missing scheduled job ID");
// Persist job.id with your business record before acknowledging this work.
console.log(job.id);
// Call with the stored ID when the reminder becomes obsolete.
async function cancelReminder(jobId) {
const result = await fetch(
`https://webhookscheduler.com/api/v1/jobs/${encodeURIComponent(jobId)}/cancel`,
{ method: "POST", headers: { Authorization: `Bearer ${apiKey}` } },
);
if (!result.ok) throw new Error(`Cancellation not confirmed: ${result.status}`);
}
Cancellation succeeds only while a job is PENDING or RETRYING. The API documents 400 for a job that is no longer cancelable, 404 for a job not found, and 409 when the state changes before cancellation is applied. These responses do not prove a business action succeeded. Inspect the job and keep the current-state check at the receiver: an HTTP request already in flight cannot be retracted.
A successful schedule call and a successful local database write are separate events. If the API accepted the job but your process crashed before recording its ID, the remote job still exists. Retry the schedule with the same idempotency key to recover the retained original job, then save its ID. The key is not a request to update the original payload.
Treat cancellation as an optimization
When an upgrade is confirmed, mark the reminder obsolete in your own database and request cancellation using the stored ID. Keep a retryable record of that request if the scheduling service is temporarily unavailable. Do not make the customer's upgrade depend on successful cleanup of the reminder.
The receiver remains the final place to check relevance. Authenticate the incoming request using the documented signature verification procedure, load the current trial record, and skip the notification if the trial is no longer eligible. Cancellation and that state check serve different purposes.
Separate delivery retries from email retries
An HTTP delivery can occur more than once. Claim the business event using a unique identifier or database transaction, and use your email provider's idempotency support where it exists. A plain check of a sent flag followed by an email call is not atomic.
There is still a boundary between your database and the email provider. Decide how to reconcile a timeout after the provider may already have accepted the message. Neither a database claim nor a scheduler can promise exactly-once email delivery across that boundary. Also define the acceptable behavior if an upgrade happens immediately after the last eligibility check.
Choose the smallest system you can operate
WebhookScheduler is built by AllClearStack. It is useful when the missing piece is delayed HTTP delivery and your team wants to keep queue operation out of the product. Your application still owns trial state, receiver security and the decision to contact a customer.
A database worker is a reasonable alternative when you already run one and an occasional scan for due reminders meets your timing needs. Before adding a service, check whether the existing worker already gives you retries and enough visibility to diagnose a missed reminder. Use the workflow templates to map those responsibilities before choosing the infrastructure.
Disclosure · Built by the AllClearStack team
When ownership is the expensive part
Webhook Scheduler handles delayed HTTP delivery, automatic retries, per-attempt logs, and one-call cancellation. Try a real delivery without creating an account.