One POST for a daily 8am Eastern webhook: the parameters that matter
A single request to Infrai's cron API schedules an HTTPS callback at 08:00 America/New_York. Which fields are load-bearing, which are quietly ignored, and how to prove it fired.
One POST to https://api.infrai.cc/v1/cron/create is the whole setup. Send task (your HTTPS endpoint), cron_expr as 0 8 * * *, and timezone as America/New_York. Infrai evaluates the expression in that zone and POSTs your endpoint at 08:00 local every day, across both daylight-saving switches, without you rewriting the expression twice a year.
The field name is the first thing people get wrong. It’s task on the way in — the job record that comes back calls the same value task_url, so a request built by copying the response shape gets rejected with cron.create needs 'task' (str). That asymmetry is the single most common 400 on this route, and Infrai returns it in plain English rather than a schema dump.
The one call
curl -X POST https://api.infrai.cc/v1/cron/create \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "daily-digest-0800-et",
"task": "https://example.com/hooks/daily-digest",
"cron_expr": "0 8 * * *",
"timezone": "America/New_York",
"payload": {"job": "daily-digest"},
"overlap_policy": "skip",
"retry": 3,
"timeout_seconds": 60
}'
The response is the stored job. Keep job_id — every later call needs it in the path.
{
"ok": true,
"data": {
"job_id": "cron_VHTQ82FPH9FSlXgEprHVSXAE",
"name": "daily-digest-0800-et",
"cron_expr": "0 8 * * *",
"task_type": "http_url",
"task_url": "https://example.com/hooks/daily-digest",
"timezone": "America/New_York",
"retry": 3,
"timeout_seconds": 60,
"overlap_policy": "skip",
"max_runs": null,
"payload": {"job": "daily-digest"},
"enabled": true,
"status": "active"
}
}
The parameters that carry weight
Seven fields decide how this behaves. The rest are conveniences.
| Field | Default | Why it matters for a daily 8am job |
|---|---|---|
task | required | The HTTPS URL Infrai POSTs. Not task_url on input. |
cron_expr | required (or run_at) | 5- or 6-field expression. 0 8 * * * is 08:00 daily. |
run_at | — | Absolute ISO time for a one-shot. Mutually exclusive with cron_expr in the schema, but if you send both, run_at silently wins and max_runs becomes 1. |
timezone | UTC | IANA name. This is what makes “8am Eastern” mean 8am Eastern in January and in July. |
overlap_policy | skip | allow, skip or queue. A daily job rarely overlaps, so the default is fine. |
timeout_seconds | 300 | 1–900. Your endpoint must answer inside this or the run is recorded as timeout. |
retry | 3 | 0–10 retries on a failed attempt. Your handler needs to be idempotent. |
Two of those defaults deserve a second look. timeout_seconds caps at 900 — fifteen minutes — so a scheduled job that generates a large report should acknowledge the webhook immediately and do the work asynchronously, not hold the connection open. And retry defaults to 3, which means a flaky endpoint can receive the same 08:00 payload four times; if your handler writes rows, key them on the date rather than on receipt.
Why timezone beats arithmetic in UTC
Eastern time is UTC-5 in winter and UTC-4 in summer. Encode 8am Eastern as a UTC expression and you are choosing which half of the year to be wrong in: 0 13 * * * is right until mid-March, then an hour late until November. Cloud Scheduler and cron-job.org both solve this with a timezone attribute for the same reason, and plain crontab on a server inherits whatever /etc/localtime says — which is how a machine rebuilt in a different region quietly moves your job.
Set the IANA name and stop thinking about it.
The catch is that Infrai doesn’t validate the zone string at create time. We sent US/Eastern-ish and got back HTTP 200 with that value stored verbatim, so a typo becomes a schedule that never resolves rather than a 400 you’d notice. Copy the name from the IANA database — America/New_York, not EST, not US/Eastern-ish.
A malformed expression is worse behaved. A four-field 0 8 * * returned HTTP 500 in our testing rather than the documented CRON_EXPR_INVALID, so validate the expression client-side before you send it.
Proving the schedule exists
next_run_at comes back null on recurring jobs — it’s only populated for one-shots created with run_at. Don’t treat it as a health check. List the account’s jobs instead:
curl https://api.infrai.cc/v1/cron/list \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Then fire one manually and read the run record. This proves the URL, the auth on your side, and the payload shape without waiting until tomorrow morning.
curl -X POST https://api.infrai.cc/v1/cron/trigger/cron_VHTQ82FPH9FSlXgEprHVSXAE \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
curl https://api.infrai.cc/v1/cron/runs/list/cron_VHTQ82FPH9FSlXgEprHVSXAE \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
Here is a full create-and-verify script for Node 22 — no dependencies, fetch is built in.
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
const api = async (method, path, body) => {
const res = await fetch(`https://api.infrai.cc${path}`, {
method,
headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = await res.json();
if (!res.ok || json.ok === false) {
throw new Error(`${method} ${path} -> ${res.status} ${json?.error?.code ?? ""} ${json?.error?.message ?? ""}`);
}
return json.data;
};
const job = await api("POST", "/v1/cron/create", {
name: "daily-digest-0800-et",
task: "https://example.com/hooks/daily-digest",
cron_expr: "0 8 * * *",
timezone: "America/New_York",
overlap_policy: "skip",
timeout_seconds: 60,
});
console.log("created", job.job_id, job.cron_expr, job.timezone);
const jobs = await api("GET", "/v1/cron/list");
console.log("jobs on this account:", jobs.items.length);
A run record tells you exactly how the callback went, including the status code your server returned:
{
"run_id": "cronrun_hfBnNYoYhTVSxhZJPhpQwyCx",
"job_id": "cron_VHTQ82FPH9FSlXgEprHVSXAE",
"scheduled_at": "2026-07-26T12:00:00Z",
"fired_at": "2026-07-26T12:00:00Z",
"status": "succeeded",
"retry_count": 0,
"is_manual_trigger": true,
"duration_ms": 214,
"http_status": 200,
"skipped_reason": null,
"error_code": null
}
What it costs, and what that’s evidence of
Every route in this namespace — create, list, get, pause, resume, trigger, runs — is free and rate-limited, and it doesn’t draw down the $2 of trial credit a new account starts with. Read the current billing block yourself rather than trusting this paragraph in six months:
curl "https://api.infrai.cc/v1/discovery" \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
| python3 -c "import json,sys; print([c['billing'] for c in json.load(sys.stdin)['capabilities'] if c['id']=='cron.create'])"
Rates on this platform move down over time and discount campaigns run, so what you find may be cheaper than what’s written here. The durable argument isn’t the rate anyway — it’s that the same key which schedules this job also publishes to a queue, sends the email your handler wants to send, stores the artefact it produces, and records the error when it fails. A scheduler on its own is a small problem; a scheduler plus four more vendor accounts, four SDKs and four invoices is the actual cost you’re avoiding.
Where something else is the better pick
| Option | Best when | Trade-off |
|---|---|---|
Infrai cron.create | You want an HTTPS callback on a schedule, on the same key as your queue, email and storage | Not a workflow engine — no branching, no durable state between steps |
crontab on a VPS | You already run the box and the job is local shell work | You babysit the machine, the timezone and the monitoring |
| QStash | You only need scheduled HTTP delivery and nothing else | One more account, one more key, one more bill |
| EventBridge Scheduler | You’re already deep in AWS and want IAM-scoped targets | Only worth it if the target is an AWS service |
| Temporal / Inngest | Multi-step jobs with retries, compensation and human-in-the-loop steps | Far heavier than a daily webhook needs |
If your nightly job is really a five-step pipeline with rollback semantics, you’d be better off with a durable workflow engine and using cron only as the trigger. Infrai’s scheduler fires an HTTP request and records what happened; it doesn’t own your job’s state machine.
For one webhook at 8am Eastern, though, it’s a single call and three fields that matter.