PUT /v1/apps/{app}/hosts
Gives each port in hosts its address, the primary port first: the port's stored label when it has one (its access and cookies are updated), else the first free label of its candidates. The primary port gets the application's own label (its name, then <name>-<org slug>, then <name>-<6 hex of the org id>, or <name>-<6 hex of the project id> outside the application's production project); a further port gets <label>--<service> (<label>--<service>-<port> when the service has several). host asks for one label instead, with the same checks. A label never moves to another port and is never given to another organization, also after b3os app down. A port asking access: public whose address serves a sandbox without spend_cap_usd is refused (app_host_public_needs_spend_cap), and one asking access: link whose address serves a sandbox with that port public is refused (app_host_port_public); nothing is written: send it once the address serves a sandbox that fits. A port without host whose every candidate holds a word no address may hold is refused with "set host on the port". b3os apply calls this; b3os plan previews it. b3os apply and the GitHub App send deploy_id, the deploy the claim is part of: each address is stamped with it, and a claim from a deploy that began before the one that last claimed or switched the address is refused (409 app_host_route_conflict, reason newer_deploy; nothing is written), so an older deploy still running never puts back what a newer one set. A claim without deploy_id (the dashboard, a call by hand) is your own change: it is never refused this way, and every deploy that began before it is. Off by default: the platform.app_hosts flag must be on for your organization, or your organization must hold a serving address; without either this answers 404 feature_not_enabled (and b3os apply keeps each port's preview URL). 503 flags_unavailable when that cannot be read.

Authentication

Bearer Token (b3os_sk_ API key or b3os_pat_ token)

Path Parameters

app string required path
The application's name (name in its b3os.yaml).
Min length 1Max length 63Pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$
Example: "research-agent"

Request Body required

application/json
hosts object[] REQUIRED
The application's addressed ports, the primary one first.
Min items 1Max items 32
Array of:
service string REQUIRED
Min length 1Max length 63Pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$
port integer REQUIRED
Min 1Max 65535
access string
link: people who hold a link from b3os app link. public: anyone on the internet.
Values link public
cookies object
mode string
isolated: the app's server gets only the cookies its own address set, plus the names in script. shared: the browser's cookies pass, except Cloudflare's and B3OS's own names and values that are not plain (a space, comma, quote or backslash).
Values isolated shared
script string[]
Max items 20Unique itemsItem pattern ^[!#$%&'*+.^_`|~0-9A-Za-z-]{1,64}$
host string
Ask for this label instead of the default; read when the port is first claimed.
Min length 1Max length 63Pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$
primary boolean
This port gets the application's own label; read when the port is first claimed.
deploy_id string
The deploy this claim is part of: a b3os apply id (<yyyymmdd>T<hhmmss>Z-<8 hex>) or a GitHub App deploy's dep_<ulid>. The claim is refused when a newer deploy, or a change without deploy_id, set an address after this deploy began. Leave it out for your own change. The preview takes it and ignores it.
Max length 64

Responses

200 The application's addresses in your project, the claimed ones included.
application/json
object string REQUIRED
Values list
data object[] REQUIRED
Array of:
object string REQUIRED
Values app_host
label string REQUIRED
An address's label, the part before .b3os.io.
url string (uri) REQUIRED
https://<label>.b3os.io.
app string REQUIRED
An application's name, the name in its b3os.yaml.
environment string REQUIRED
The environment (production; environments are projects in this beta).
project_id string REQUIRED
service string REQUIRED
The service of the application whose port this is.
port integer REQUIRED
access string REQUIRED
link: people who hold a link from b3os app link. public: anyone on the internet.
Values link public
cookies object REQUIRED
mode string REQUIRED
isolated: the app's server gets only the cookies its own address set, plus the names in script. shared: the browser's cookies pass, except Cloudflare's and B3OS's own names and values that are not plain (a space, comma, quote or backslash).
Values isolated shared
script string[] REQUIRED
Cookies a page script writes and the app's server reads, by name.
state string REQUIRED
claimed: the label is the port's and no deploy has switched it yet. serving: it serves sandbox_id. stopped: it serves nothing (b3os app down, or the organization or project stopped). blocked: B3 took it down; it can be neither claimed nor routed.
Values claimed serving stopped blocked
sandbox_id string | null REQUIRED
The sandbox the address serves (serving only).
deploy_id string | null REQUIRED
The deploy that last switched the address.
base_label string | null REQUIRED
The application's own label this one is derived from (<base>--<service>), or null.
primary boolean
The address of the application's primary port, which holds its own label (the others derive from it). Set when that port is first claimed; a later primary in b3os.yaml does not move it. Every answer of the edge has it.
blocked_reason string | null
Why B3 blocked the address (blocked only; else null). Every answer of the edge has it.
claimed_at string (date-time) REQUIRED
updated_at string (date-time) REQUIRED
dropped_cookies object[] REQUIRED
Cookies a page script set that the address did not pass to the app in the last 7 days (cookies: isolated), by name, most requests first. List one under cookies.script to let it through.
Array of:
name string REQUIRED
requests integer REQUIRED
next_cursor string | null REQUIRED
400 The request failed validation (invalid_request, invalid_url).
401 Missing, malformed, expired or revoked credentials (unauthorized).
403 B3 blocked the address; it can be neither claimed nor routed (app_host_blocked).
404 Addresses are not on for your organization (feature_not_enabled: the platform.app_hosts flag). Nothing was read or claimed.
409 Every candidate label is held by another application or organization (app_host_label_taken), the port already has another label than host asks for (app_host_conflict, reason label_fixed), or a newer deploy, or a change outside a deploy, set the address after the deploy deploy_id names began (app_host_route_conflict, reason newer_deploy; nothing is written).
413 The request body exceeds this operation's limit.
415 The Content-Type is not accepted by this operation.
422 The label is one B3OS refuses (app_host_label_reserved, with reason), the claim is over apps.hosts.max or apps.hosts.public_max (limit_exceeded), it would make a serving address public while the sandbox it serves has no spend_cap_usd (app_host_public_needs_spend_cap), or it would make a serving address link while the sandbox it serves has that port public (app_host_port_public). Nothing is written.
429 A rate, concurrency or quota limit applies (rate_limited, concurrency_limited, quota_exceeded).
500 Something failed inside B3OS (internal). Retry with backoff.
503 B3OS could not read whether addresses are on for your organization (flags_unavailable: the flag platform.app_hosts, or whether your organization holds a serving address). Nothing was read or changed; retry after Retry-After seconds. A caller must not read this as "no addresses".
curl -X PUT 'https://beta-api.b3os.org/v1/apps/research-agent/hosts' \  -H 'Authorization: Bearer YOUR_API_TOKEN' \  -H 'Content-Type: application/json' \  -d '{  "hosts": [    {      "service": "web",      "port": 8000,      "access": "link",      "cookies": {        "mode": "isolated",        "script": [          "theme"        ]      },      "primary": true    },    {      "service": "api",      "port": 9000,      "access": "link"    }  ]}'
const response = await fetch('https://beta-api.b3os.org/v1/apps/research-agent/hosts', {  method: 'PUT',  headers: {      "Authorization": "Bearer YOUR_API_TOKEN",      "Content-Type": "application/json"  },  body: JSON.stringify({    "hosts": [      {        "service": "web",        "port": 8000,        "access": "link",        "cookies": {          "mode": "isolated",          "script": [            "theme"          ]        },        "primary": true      },      {        "service": "api",        "port": 9000,        "access": "link"      }    ]  })});const data = await response.json();console.log(data);
import requestsheaders = {    'Authorization': 'Bearer YOUR_API_TOKEN'}response = requests.put('https://beta-api.b3os.org/v1/apps/research-agent/hosts', headers=headers, json={  "hosts": [    {      "service": "web",      "port": 8000,      "access": "link",      "cookies": {        "mode": "isolated",        "script": [          "theme"        ]      },      "primary": true    },    {      "service": "api",      "port": 9000,      "access": "link"    }  ]})print(response.json())
200 Response
{  "object": "list",  "data": [    {      "object": "app_host",      "label": "gpt-researcher",      "url": "https://gpt-researcher.b3os.io",      "app": "gpt-researcher",      "environment": "production",      "project_id": "prj_01m5t3pf8heewxt65ej3dm577y",      "service": "web",      "port": 8000,      "access": "link",      "cookies": {        "mode": "isolated",        "script": [          "theme"        ]      },      "state": "claimed",      "sandbox_id": null,      "deploy_id": null,      "base_label": null,      "primary": true,      "blocked_reason": null,      "claimed_at": "2026-11-11T09:00:00.000Z",      "updated_at": "2026-11-11T09:00:00.000Z",      "dropped_cookies": []    },    {      "object": "app_host",      "label": "gpt-researcher--api",      "url": "https://gpt-researcher--api.b3os.io",      "app": "gpt-researcher",      "environment": "production",      "project_id": "prj_01m5t3pf8heewxt65ej3dm577y",      "service": "api",      "port": 9000,      "access": "link",      "cookies": {        "mode": "isolated",        "script": []      },      "state": "claimed",      "sandbox_id": null,      "deploy_id": null,      "base_label": "gpt-researcher",      "primary": false,      "blocked_reason": null,      "claimed_at": "2026-11-11T09:00:00.000Z",      "updated_at": "2026-11-11T09:00:00.000Z",      "dropped_cookies": []    }  ],  "next_cursor": null}
Ask a question... ⌘I