When a user starts a trial, schedule the downgrade for when it ends.
const res = await fetch("https://run-later.com/api/schedule", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer rl_live_xxx"
},
body: JSON.stringify({
target_url: "https://yourapp.com/api/downgrade",
execute_at: trialEndDate.toISOString(),
payload: { user_id: "u_123", plan: "free" },
}),
});
// Response (201 Created):
// { "id": "job_xxx" }
//
// Error (401): { "error": "Unauthorized" }
// Error (400): { "error": "Invalid request" }
Soft-delete immediately, schedule hard-delete in 30 minutes. Cancel if the user clicks undo.
const deleteAt = new Date(Date.now() + 30 * 60 * 1000);
const { id: jobId } = await fetch("https://run-later.com/api/schedule", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer rl_live_xxx"
},
body: JSON.stringify({
target_url: "https://yourapp.com/api/hard-delete",
execute_at: deleteAt.toISOString(),
payload: { record_id: "rec_456" },
}),
}).then(r => r.json());
// Store jobId so you can show "undo" in your UI
// (RunLater MVP does not support cancellation yet -
// your hard-delete endpoint should check if the
// record was restored before deleting)
RunLater currently does not support job cancellation after scheduling.
If you need conditional behavior (for example, undo delete), your endpoint should verify current state before executing destructive actions.
Every webhook delivery includes these headers:
X-RunLater-Job-ID - the job identifier (matches the id returned by /api/schedule)X-RunLater-Timestamp - Unix timestamp of the delivery attemptX-RunLater-Signature - HMAC-SHA256 signature for payload verificationUse X-RunLater-Timestamp and X-RunLater-Signature to verify the request.
const crypto = require("crypto");
function verifyRunLater(req, secret) {
const timestamp = req.headers["x-runlater-timestamp"];
const signature = req.headers["x-runlater-signature"];
const body = req.rawBody; // exact request body string
// Reject old timestamps (5 min window)
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return false;
}
const expected = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex")
);
}
Your server must use the exact raw request body string (before JSON parsing). Many frameworks require explicit middleware to access the raw body.
The signature is computed as:
HMAC-SHA256(your_webhook_secret, timestamp + "." + raw_request_body)
Your webhook secret is available on the Settings page. Always use constant-time comparison. Reject timestamps older than 5 minutes.
The API accepts two authentication methods:
Authorization: Bearer rl_live_xxxYour API key is available on the Settings page.
RunLater attempts delivery once at the scheduled minute. Seconds are accepted but sub-minute precision is not guaranteed.
A delivery is considered successful if your endpoint responds with a 2xx HTTP status code.
If the request fails due to a network error, timeout, or HTTP 5xx response, it is retried once ~60 seconds later.
No further retries are performed. If the second attempt also fails, the job is marked as failed.
RunLater does not provide automatic idempotency. If you retry scheduling from your system, you are responsible for preventing duplicate jobs.
Webhook requests are limited to 10 seconds. If your endpoint does not respond within 10 seconds, the attempt is treated as a failure.
All timestamps must be ISO 8601 in UTC (for example: 2026-03-01T12:00:00Z).
Dashboard timestamps can be toggled between UTC and local time.
RunLater is designed for lightweight automation workflows and is not intended for mission-critical systems.