Skip to content
Docs

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.com

Requests 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:

TokenUse it fromTeam and project
OIDC tokenCode running in a Edgenivo deployment, in VERCEL_OIDC_TOKENTaken from the token. Don't pass teamId or projectId.
Edgenivo access tokenScripts, CI, and external systemsPass projectId in the query string. Pass teamId unless the project belongs to your default team.

For example, with an access token:

terminal
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:

terminal
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 parameterSelects
envproduction, or a custom environment by name or ID
branchA Preview branch of the project's connected Git repository
userCLI 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
}
FieldTypeDescription
scheduleIdstringSchedule ID
ownerIdstringID of the team that owns the schedule
projectIdstringID of the project
namestringSchedule name
namespacestringSchedule namespace
timezonestringIANA timezone. Defaults to UTC.
expressionobject{ "type": "cron", "cron": string }, or { "type": "single", "at": string } for a one-time schedule
jitterintegerJitter window in minutes. Omitted when the schedule uses the plan default.
targetobject{ "type": "queue", "topic": string }. Static schedules can also have { "type": "function", "function": string, "service"?: string } or { "type": "path", "path": string, "requestMethod"?: "GET" | "POST" }.
statestringactive or inactive
activeUntilintegerUnix timestamp in milliseconds when a temporarily enabled schedule becomes inactive. Only present while that window is open.
sourcestringstatic for schedules from your configuration, dynamic for schedules created with the API
createdAtintegerUnix timestamp in milliseconds
updatedAtintegerUnix timestamp in milliseconds

Responses don't include a schedule's payload.

MethodPathDescription
GET/v1/schedulesList schedules
POST/v1/schedulesCreate 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}/enableEnable a schedule
POST/v1/schedules/{name}/disableDisable a schedule
POST/v1/schedules/{name}/invokeRun a schedule now

GET /v1/schedules

Lists static and dynamic schedules in one namespace, one page at a time.

Query parameterTypeRequiredDescription
namespacestringNoNamespace to list. Defaults to default.
limitintegerNoPage size, from 1 to 100. Defaults to 20.
cursorstringNocursor 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.

terminal
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 fieldTypeRequiredDescription
namestringNoUnique within the namespace. See naming rules. Edgenivo generates a name when you omit it.
namespacestringNoDefaults to default
expressionobjectYes{ "type": "cron", "cron": string } with a five-field cron expression, or { "type": "single", "at": string } with a local date and time
targetobjectYes{ "type": "queue", "topic": string }. Dynamic schedules only support queue topics.
timezonestringNoIANA timezone for the expression. Defaults to UTC.
jitterintegerNoJitter window in minutes, from 1 to 15. On Hobby, omit it or pass 15.
payloadany JSON valueNoUp 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:

terminal
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 fieldTypeDescription
expressionobjectNew expression of the same type. You can't change a cron schedule to a one-time schedule, or the reverse.
targetobjectNew queue topic, as { "type": "queue", "topic": string }
timezonestringNew IANA timezone
jitterinteger or nullNew jitter window. Pass null to return to the plan default.
payloadany JSON value or nullNew payload. Pass null to remove it.

Returns 200 with the updated schedule object. Returns 409 with:

  • static_schedule for a static schedule. Edit your configuration and redeploy instead.
  • schedule_type_change_not_allowed if you change the expression type
  • schedule_single_at_passed for 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 fieldTypeDescription
untilintegerUnix 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"
  }
}
StatusCodes
400bad_request, invalid_cron_expression, invalid_jitter, jitter_exceeds_cadence, single_at_in_past, payload_too_large, active_until_in_past, schedule_limit_exceeded
403forbidden, not_enabled
404not_found
409schedule_already_exists, schedule_already_active, schedule_single_at_passed, schedule_type_change_not_allowed, static_schedule
429rate_limited, feature_blocked
503service_unavailable

The JS SDK Reference describes what causes each code.

  • 429 with rate_limited: You exceeded an API request limit. Wait for the number of seconds in the Retry-After header. The response also includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset.
  • 429 with feature_blocked: Your Hobby team went over its usage limits. Retrying doesn't help until the next billing period.
  • 503 with service_unavailable: Wait for the number of seconds in the Retry-After header and retry. A write may have been applied before the error, so a retried create can return 409 with schedule_already_exists, and a retried delete can return 404. Treat those as success.

Was this helpful?

supported.