Edgenivo Schedules: API Reference
The Schedules HTTP API is a REST API for managing static and dynamic schedules. The @vercel/schedules JS SDK and the vercel schedules CLI both use it. Call it directly when you can't use the SDK, such as from another language or an external system.
All endpoints use this base URL:
https://vercel-schedules.comRequests and responses use JSON. Send Content-Type: application/json with every request that has a body.
Send a token in the Authorization header:
Authorization: Bearer <token>The API accepts two kinds of tokens:
| Token | Use it from | Team and project |
|---|---|---|
| OIDC token | Code running in a Edgenivo deployment, in VERCEL_OIDC_TOKEN | Taken from the token. Don't pass teamId or projectId. |
| Edgenivo access token | Scripts, CI, and external systems | Pass projectId in the query string. Pass teamId unless the project belongs to your default team. |
For example, with an access token:
curl "https://vercel-schedules.com/v1/schedules?teamId=team_a1b2c3&projectId=prj_d4e5f6" \
-H "Authorization: Bearer $VERCEL_TOKEN"From a deployment, with its OIDC token:
curl "https://vercel-schedules.com/v1/schedules" \
-H "Authorization: Bearer $VERCEL_OIDC_TOKEN"A request with a missing or invalid token fails with 403 and code forbidden. An access token can only manage schedules that its user's role allows. See Permissions.
Edgenivo keeps a separate set of schedules for each environment. With an access token, choose one with a query parameter. Without one, requests use production.
| Query parameter | Selects |
|---|---|
env | production, or a custom environment by name or ID |
branch | A Preview branch of the project's connected Git repository |
user | CLI deployments by a Edgenivo user ID. Combine with branch for CLI deployments associated with a branch. |
Pass at most one of these, except user with branch. If the custom environment, branch, or user doesn't have a deployment yet, the request fails with 400.
OIDC tokens always use the environment of the deployment that issued them. Requests with an OIDC token can't pass env, branch, or user.
Every endpoint that reads or changes one schedule, and the list endpoint, accepts a namespace query parameter. It defaults to default. When you create a schedule, pass namespace in the request body instead. See Multi-tenant schedules.
Endpoints that return a schedule use this shape:
{
"scheduleId": "sch_01JC4Z8Q3W5V7X9Y2A4B6C8D0E",
"ownerId": "team_a1b2c3",
"projectId": "prj_d4e5f6",
"name": "nightly-report",
"namespace": "reports",
"timezone": "America/New_York",
"expression": { "type": "cron", "cron": "0 3 * * *" },
"jitter": 5,
"target": { "type": "queue", "topic": "nightly-report" },
"state": "active",
"source": "dynamic",
"createdAt": 1793750400000,
"updatedAt": 1793750400000
}| Field | Type | Description |
|---|---|---|
scheduleId | string | Schedule ID |
ownerId | string | ID of the team that owns the schedule |
projectId | string | ID of the project |
name | string | Schedule name |
namespace | string | Schedule namespace |
timezone | string | IANA timezone. Defaults to UTC. |
expression | object | { "type": "cron", "cron": string }, or { "type": "single", "at": string } for a one-time schedule |
jitter | integer | Jitter window in minutes. Omitted when the schedule uses the plan default. |
target | object | { "type": "queue", "topic": string }. Static schedules can also have { "type": "function", "function": string, "service"?: string } or { "type": "path", "path": string, "requestMethod"?: "GET" | "POST" }. |
state | string | active or inactive |
activeUntil | integer | Unix timestamp in milliseconds when a temporarily enabled schedule becomes inactive. Only present while that window is open. |
source | string | static for schedules from your configuration, dynamic for schedules created with the API |
createdAt | integer | Unix timestamp in milliseconds |
updatedAt | integer | Unix timestamp in milliseconds |
Responses don't include a schedule's payload.
| Method | Path | Description |
|---|---|---|
GET | /v1/schedules | List schedules |
POST | /v1/schedules | Create a dynamic schedule |
GET | /v1/schedules/{name} | Get a schedule |
PATCH | /v1/schedules/{name} | Update a dynamic schedule |
DELETE | /v1/schedules/{name} | Delete a dynamic schedule |
POST | /v1/schedules/{name}/enable | Enable a schedule |
POST | /v1/schedules/{name}/disable | Disable a schedule |
POST | /v1/schedules/{name}/invoke | Run a schedule now |
GET /v1/schedules
Lists static and dynamic schedules in one namespace, one page at a time.
| Query parameter | Type | Required | Description |
|---|---|---|---|
namespace | string | No | Namespace to list. Defaults to default. |
limit | integer | No | Page size, from 1 to 100. Defaults to 20. |
cursor | string | No | cursor from the previous page |
Returns 200:
{
"data": [
{
"scheduleId": "sch_01JC4Z8Q3W5V7X9Y2A4B6C8D0E",
"ownerId": "team_a1b2c3",
"projectId": "prj_d4e5f6",
"name": "nightly-report",
"namespace": "reports",
"timezone": "America/New_York",
"expression": { "type": "cron", "cron": "0 3 * * *" },
"jitter": 5,
"target": { "type": "queue", "topic": "nightly-report" },
"state": "active",
"source": "dynamic",
"createdAt": 1793750400000,
"updatedAt": 1793750400000
}
],
"cursor": "eyJQSyI6Ii4uLiIsIlNLIjoiLi4uIn0"
}data holds schedule objects, ordered by name. When cursor is null, there are no more pages. A page can hold fewer than limit schedules, or none, and still have a cursor, so keep reading until cursor is null.
POST /v1/schedules
Creates a dynamic schedule that publishes to a queue topic. It's active as soon as you create it.
curl -X POST "https://vercel-schedules.com/v1/schedules?teamId=team_a1b2c3&projectId=prj_d4e5f6" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "nightly-report",
"namespace": "reports",
"expression": { "type": "cron", "cron": "0 3 * * *" },
"timezone": "America/New_York",
"jitter": 5,
"target": { "type": "queue", "topic": "nightly-report" },
"payload": { "report": "daily-summary" }
}'| Body field | Type | Required | Description |
|---|---|---|---|
name | string | No | Unique within the namespace. See naming rules. Edgenivo generates a name when you omit it. |
namespace | string | No | Defaults to default |
expression | object | Yes | { "type": "cron", "cron": string } with a five-field cron expression, or { "type": "single", "at": string } with a local date and time |
target | object | Yes | { "type": "queue", "topic": string }. Dynamic schedules only support queue topics. |
timezone | string | No | IANA timezone for the expression. Defaults to UTC. |
jitter | integer | No | Jitter window in minutes, from 1 to 15. On Hobby, omit it or pass 15. |
payload | any JSON value | No | Up to 16 KB. Edgenivo includes it in the payload field of each queue message. |
The body can't contain other fields. Returns 201 with the new schedule object. Returns 409 with schedule_already_exists if a schedule with the same name exists in the namespace.
GET /v1/schedules/{name}
Returns 200 with one static or dynamic schedule object. Pass namespace in the query string for a schedule outside the default namespace. Returns 404 with not_found if the schedule doesn't exist.
PATCH /v1/schedules/{name}
Changes a dynamic schedule. Fields you leave out keep their current values:
curl -X PATCH "https://vercel-schedules.com/v1/schedules/nightly-report?namespace=reports&teamId=team_a1b2c3&projectId=prj_d4e5f6" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "expression": { "type": "cron", "cron": "0 4 * * *" }, "jitter": null }'| Body field | Type | Description |
|---|---|---|
expression | object | New expression of the same type. You can't change a cron schedule to a one-time schedule, or the reverse. |
target | object | New queue topic, as { "type": "queue", "topic": string } |
timezone | string | New IANA timezone |
jitter | integer or null | New jitter window. Pass null to return to the plan default. |
payload | any JSON value or null | New payload. Pass null to remove it. |
Returns 200 with the updated schedule object. Returns 409 with:
static_schedulefor a static schedule. Edit your configuration and redeploy instead.schedule_type_change_not_allowedif you change the expression typeschedule_single_at_passedfor a one-time schedule whose scheduled time has passed
DELETE /v1/schedules/{name}
Deletes a dynamic schedule and stops its future runs. Returns 200:
{ "scheduleId": "sch_01JC4Z8Q3W5V7X9Y2A4B6C8D0E" }Returns 409 with static_schedule for a static schedule, or with schedule_single_at_passed for a one-time schedule whose run is still being dispatched. See Delete a schedule for what happens to pending runs.
POST /v1/schedules/{name}/enable
Makes a static or dynamic schedule run on its cadence. The body is optional. To enable the schedule until a set time, pass until:
{ "until": 1793757600000 }| Body field | Type | Description |
|---|---|---|
until | integer | Unix timestamp in milliseconds when the schedule becomes inactive again. Must be in the future. |
Returns 200 with the updated schedule object. Enabling without until keeps the schedule active indefinitely and clears any activeUntil. Passing until replaces an existing end time, but fails with 409 and schedule_already_active if the schedule is already active indefinitely. Disable it first.
POST /v1/schedules/{name}/disable
Stops a static or dynamic schedule from running on its cadence. Send no body. Returns 200 with the updated schedule object.
POST /v1/schedules/{name}/invoke
Runs a static or dynamic schedule now, without changing its state or cadence. Works on inactive schedules. Send no body.
Returns 204 with no body once Edgenivo accepts the run. Your target receives it shortly afterward. Each request starts a new run, so don't retry a request that succeeded.
Failed requests return a JSON body with a machine-readable code and a message:
{
"error": {
"code": "schedule_already_exists",
"message": "Schedule already exists"
}
}| Status | Codes |
|---|---|
400 | bad_request, invalid_cron_expression, invalid_jitter, jitter_exceeds_cadence, single_at_in_past, payload_too_large, active_until_in_past, schedule_limit_exceeded |
403 | forbidden, not_enabled |
404 | not_found |
409 | schedule_already_exists, schedule_already_active, schedule_single_at_passed, schedule_type_change_not_allowed, static_schedule |
429 | rate_limited, feature_blocked |
503 | service_unavailable |
The JS SDK Reference describes what causes each code.
429withrate_limited: You exceeded an API request limit. Wait for the number of seconds in theRetry-Afterheader. The response also includesX-RateLimit-Limit,X-RateLimit-Remaining, andX-RateLimit-Reset.429withfeature_blocked: Your Hobby team went over its usage limits. Retrying doesn't help until the next billing period.503withservice_unavailable: Wait for the number of seconds in theRetry-Afterheader and retry. A write may have been applied before the error, so a retried create can return409withschedule_already_exists, and a retried delete can return404. Treat those as success.
Was this helpful?