Claim an application's addresses
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/jsonhosts
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 65535access
string
link: people who hold a link from b3os app link. public: anyone on the internet.Values
link publiccookies
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 sharedscript
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
64Responses
200
The application's addresses in your project, the claimed ones included.
application/jsonobject
string
REQUIRED
Values
listdata
object[]
REQUIRED
Array of:
object
string
REQUIRED
Values
app_hostlabel
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 publiccookies
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 sharedscript
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 blockedsandbox_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}
API Playground
Try this endpoint
PUT
/v1/apps/{app}/hosts