On this page
Migrating from subhosting API v1 to v2
This guide covers migrating from the Deno Deploy subhosting API v1
(api.deno.com/v1) to v2 (api.deno.com/v2). The v1 API will be shut down on
July 20, 2026. For general platform migration (apps, domains, environment
variables), see the Deploy Classic migration guide.
Full v2 API reference: api.deno.com/v2/docs
Official SDKs:
- TypeScript/JavaScript: @deno/sandbox
- Python: deno-sandbox on PyPI (source)
These SDKs are branded as "sandbox" but work for subhosting use cases.
Why migrate to v2 Jump to heading
V2 runs on an entirely new platform with significant improvements:
- No per-request CPU time limits (no more
TIME_LIMITerrors) - Configurable memory limits, up to 4 GB (v1 is fixed at 512 MB)
- Custom OpenTelemetry export (logs, metrics, traces) to your own endpoint
- Custom build steps and framework support (Next.js, Astro, SvelteKit, etc.)
- Built-in HTTP cache
- Layers for instant bulk configuration updates across many apps without redeploying
- Web Application Firewall (WAF)
Key concept changes Jump to heading
| v1 | v2 | Notes |
|---|---|---|
| Organization | Organization | Unchanged |
| Project | App | One app per function |
| Deployment | Revision | Immutable snapshot of code + config |
| Project name | App slug | Used in URLs; unique within the org |
| — | Labels | Key-value pairs on apps for grouping/filtering |
| — | Layers | Shared configuration (env vars) across apps |
| — | Config | Build and runtime configuration |
| — | Timeline | Deployment target within an app |
Architecture change: one app per function Jump to heading
In v1, you may have used a single project with multiple active deployments to represent separate functions. In v2, create a separate app for each function.
V2 introduces timelines. Each timeline has only one active revision at a time — deploying a new revision replaces the previous one. When deploying via the API, revisions are automatically labeled as production, so each app effectively has a single active revision.
This means v2 revisions are not a 1:1 equivalent of v1 deployments. In v1, multiple deployments within a project could be active simultaneously. In v2, only one revision per timeline is active.
Recommended approach:
- Create one app per function with a descriptive slug (e.g.
my-service-auth,my-service-billing) - Use labels to group related apps — filter with
GET /apps?labels[service]=my-service - Use layers to share environment variables across apps
Configuration inheritance Jump to heading
In v1, every deployment was self-contained: environment variables, entrypoint, and other options had to be specified in each deploy request.
In v2, apps carry configuration that revisions inherit:
- Build config (
config): install command, build command, runtime entrypoint. Set once on the app, inherited by all revisions. - Environment variables (
env_vars): set on the app or on a layer, inherited by all revisions. - Layers: set on the app, inherited by all revisions.
You can override any of these per-revision by including them in the deploy
request. If omitted, the app's configuration is used. This means you typically
only need to send assets after initial setup:
{
"assets": {
"main.ts": {
"kind": "file",
"content": "Deno.serve((req) => new Response('Hello'));"
}
}
}
Authentication Jump to heading
| Aspect | v1 | v2 |
|---|---|---|
| Token prefix | dd... |
ddo_... |
| Header | Authorization: Bearer <token> |
Authorization: Bearer <token> |
| Scope | Organization | Organization |
| Base URL | https://api.deno.com/v1 |
https://api.deno.com/v2 |
Create v2 tokens in the Deno Deploy dashboard under your organization's settings.
Endpoint mapping Jump to heading
List projects / apps Jump to heading
v1: GET /organizations/{orgId}/projects
v2: GET /apps
The v2 API scopes to the organization automatically based on your token. No organization ID needed in the URL.
# v1
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v1/organizations/$ORG_ID/projects?page=1&limit=20"
# v2
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v2/apps?limit=30"
| Aspect | v1 | v2 |
|---|---|---|
| Pagination | page + limit (page-based) |
cursor + limit (cursor-based) |
| Filtering | q (search by name/ID) |
labels, layer |
| Response | Array of Project objects | Array of AppListItem objects |
Response field mapping:
| v1 Project | v2 AppListItem |
|---|---|
id (UUID) |
id (UUID) |
name |
slug |
description |
not available |
createdAt |
created_at |
updatedAt |
updated_at |
| — | labels (object) |
| — | layers (array of {id, slug}) |
V2 uses snake_case for all field names (v1 uses camelCase).
Get project / app details Jump to heading
v1: GET /projects/{projectId}
v2: GET /apps/{app}
The v2 endpoint accepts either the app UUID or slug.
# v1
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v1/projects/$PROJECT_ID"
# v2
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v2/apps/my-app-slug"
The v2 response includes env_vars, config, labels, and layers.
Create a deployment / revision Jump to heading
v1: POST /projects/{projectId}/deployments
v2: POST /apps/{app}/deploy
This is the most significant API change.
v1 request Jump to heading
{
"entryPointUrl": "main.ts",
"assets": {
"main.ts": {
"content": "Deno.serve((req) => new Response('Hello'));",
"encoding": "utf-8"
}
},
"envVars": {
"MY_VAR": "my_value"
}
}
v2 request Jump to heading
{
"assets": {
"main.ts": {
"kind": "file",
"content": "Deno.serve((req) => new Response('Hello'));",
"encoding": "utf-8"
}
},
"config": {
"install": "deno install",
"runtime": {
"type": "dynamic",
"entrypoint": "main.ts"
}
},
"env_vars": [
{ "key": "MY_VAR", "value": "my_value" }
]
}
Key differences:
| Aspect | v1 | v2 |
|---|---|---|
| Entrypoint | Top-level entryPointUrl |
config.runtime.entrypoint |
| Assets | content+encoding or gitSha1 |
kind: "file" with content+encoding, or kind: "symlink" with target |
| Env vars | Object {"KEY": "value"} |
Array [{"key": "KEY", "value": "value"}] |
| Import map | importMapUrl field |
Not needed; Deno auto-discovers deno.json |
| Lock file | lockFileUrl field |
Not needed; handled by deno install |
| Compiler options | compilerOptions field |
Not needed; use deno.json |
| Build config | None (import caching only) | config.install and config.build |
| Response status | 200 |
202 Accepted (build is async) |
| Databases/KV | databases field |
Not available |
| Request timeout | requestTimeout field |
Not available |
| Permissions | permissions field |
Not available |
Build configuration: In v1, the "build" step only cached imports. The
equivalent in v2 is config.install: "deno install" with config.build: null.
No framework preset is needed (leave config.framework as "").
Configuration inheritance: config, env_vars, and layers are all
optional in deploy requests. When omitted, the revision inherits the app's
configuration. Set these once when creating the app and then only send assets
in subsequent deploys.
Status values Jump to heading
| v1 | v2 | Meaning |
|---|---|---|
pending |
queued |
Build not yet started |
| (none) | building |
Build in progress |
success |
succeeded |
Deployed and live |
failed |
failed |
Build failed |
| (none) | skipped |
Build was skipped |
Get deployment / revision status Jump to heading
v1: GET /deployments/{deploymentId}
v2: GET /revisions/{revisionId}
# v1
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v1/deployments/$DEPLOYMENT_ID"
# v2
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v2/revisions/$REVISION_ID"
The v2 response includes failure_reason when status is failed.
List deployments / revisions Jump to heading
v1: GET /projects/{projectId}/deployments
v2: GET /apps/{app}/revisions
# v1
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v1/projects/$PROJECT_ID/deployments?page=1&limit=20"
# v2
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v2/apps/my-app-slug/revisions?limit=30"
| Aspect | v1 | v2 |
|---|---|---|
| Pagination | page + limit |
cursor + limit |
| Filtering | q (search by ID) |
status |
Tail build logs Jump to heading
v1: GET /deployments/{deploymentId}/build_logs (polling)
v2 offers two approaches:
Stream revision progress (recommended) Jump to heading
GET /revisions/{revisionId}/progress
Emits events as the build progresses. Ends when the revision reaches a terminal
state (succeeded, failed, or skipped). Replaces the v1 pattern of polling
deployment status in a loop.
curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream" \
"https://api.deno.com/v2/revisions/$REVISION_ID/progress"
Stream build logs Jump to heading
GET /revisions/{revisionId}/build_logs
curl -H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream" \
"https://api.deno.com/v2/revisions/$REVISION_ID/build_logs"
| Aspect | v1 | v2 |
|---|---|---|
| Formats | application/x-ndjson, application/json |
text/event-stream (SSE), application/x-ndjson |
| Log fields | level, message |
timestamp, level, message, step, timeline |
| Filter by step | Not supported | step param (preparing/installing/building/deploying) |
Query app logs Jump to heading
v1: GET /deployments/{deploymentId}/app_logs
v2: GET /apps/{app}/logs
In v1, logs are scoped to a single deployment. In v2, logs are scoped to an app
with optional filtering by revision_id.
# v1
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v1/deployments/$DEPLOYMENT_ID/app_logs?since=2024-01-01T00:00:00Z&until=2024-01-01T01:00:00Z"
# v2
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v2/apps/my-app-slug/logs?start=2024-01-01T00:00:00Z&end=2024-01-01T01:00:00Z"
Parameter mapping:
| v1 | v2 | Notes |
|---|---|---|
since |
start |
Required in v2 |
until |
end |
Optional |
limit |
limit |
v1: max 10000. v2: max 1000 |
order |
(none) | v2 returns chronological order |
q |
query |
Text search |
level |
level |
v1: warning. v2: warn |
region |
(none) | Region filtering not available in v2 |
Response field mapping:
| v1 field | v2 field |
|---|---|
time |
timestamp |
level |
level |
message |
message |
region |
region |
| — | revision_id |
| — | trace_id |
| — | span_id |
V2 wraps logs in an object with next_cursor for pagination:
{
"logs": [
{
"timestamp": "2024-01-01T00:00:01Z",
"level": "info",
"message": "Listening on http://0.0.0.0:8000",
"region": "us-east-1",
"revision_id": "abcdef12"
}
],
"next_cursor": "eyJsYXN0X3RzIjoiMjAy..."
}
Streaming: In v1, omitting since and until enables real-time streaming.
In v2, omit the end parameter to stream. Set the Accept header to
text/event-stream for SSE format or application/x-ndjson for NDJSON.
Requesting application/json without end returns an error.
Using labels to group apps Jump to heading
Labels replace the v1 pattern of grouping multiple deployments under a single project:
# Create an app with labels
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://api.deno.com/v2/apps" \
-d '{
"slug": "my-service-auth",
"labels": {
"service": "my-service",
"function": "auth"
},
"config": {
"install": "deno install",
"runtime": {
"type": "dynamic",
"entrypoint": "main.ts"
}
}
}'
# List all apps in a service
curl -H "Authorization: Bearer $TOKEN" \
"https://api.deno.com/v2/apps?labels[service]=my-service"
Using layers for shared configuration Jump to heading
Layers let you share environment variables across multiple apps. This was not possible in v1, where env vars had to be duplicated in every deployment request.
# Create a layer with shared env vars
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://api.deno.com/v2/layers" \
-d '{
"slug": "my-service-shared",
"env_vars": [
{"key": "DATABASE_URL", "value": "postgres://...", "secret": true},
{"key": "API_KEY", "value": "sk-...", "secret": true}
]
}'
# Create an app that uses the layer
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
"https://api.deno.com/v2/apps" \
-d '{
"slug": "my-service-auth",
"layers": ["my-service-shared"],
"labels": {"service": "my-service"},
"config": {
"install": "deno install",
"runtime": {
"type": "dynamic",
"entrypoint": "main.ts"
}
}
}'
When a layer's env vars are updated, all apps using that layer pick up the changes immediately.
Migration checklist Jump to heading
- Create a v2 organization token in the Deno Deploy dashboard (
ddo_prefix). - Create one app per function instead of deploying multiple functions to a single project. Use labels to group related apps.
- Set up layers for environment variables shared across functions.
- Update deploy requests:
- Move
entryPointUrltoconfig.runtime.entrypoint - Set
config.installto"deno install" - Convert
envVarsobject toenv_varsarray, or set on the app/layer - Add
kind: "file"to assets
- Move
- Update build log tailing to use
GET /revisions/{id}/progress(SSE) instead of polling. - Update log queries:
since/until→start/endwarning→warntime→timestampin log response body- Handle
{logs: [...], next_cursor: ...}response wrapper
- Update pagination from page-based to cursor-based.
- Update status values:
pending→queued,success→succeeded. - Update field names from camelCase to snake_case.