Web Hosting
Create PHP hosting sites and manage their domains, redirects, files and MySQL databases.
Overview
A hosting site runs PHP on Apache in its own container and can answer on several hostnames at once. This page covers the API; the Web Hosting guide explains the folder layout, SFTP and how PHP runs.
All endpoints are project-scoped and sit under:
/v1/projects/:projectId/hosting/sharedAuth: Required. Read endpoints need the hosting:read scope and write
endpoints hosting:write. A key scoped for dedicated servers does not reach
hosting sites.
Sites
List sites
GET /v1/projects/:projectId/hosting/sharedReturns every site in the project that has not been deleted.
Get a site
GET /v1/projects/:projectId/hosting/shared/:tenantIdReturns the site with its limits, its address (url), the SFTP host and port,
and ready, which says whether the site is serving right now. status says
what was provisioned; ready is read live.
Add ?credentials=true to include sftpPassword. It is left out otherwise, so
a page that shows connection details never has to fetch a password.
Get connection details
GET /v1/projects/:projectId/hosting/shared/:tenantId/accessResponse:
{
"success": true,
"data": {
"username": "h_mysite_x7k2mf",
"host": "sftp.cosmoner.com",
"sftp": { "port": 2022 },
"ssh": { "port": 2022, "enabled": false }
}
}SFTP and SSH share one host and port, the same for every site, and the port is never 22. The username picks the site. Plain FTP and FTPS are not offered.
List tier prices
GET /v1/projects/:projectId/hosting/shared/pricesReturns the monthly list price for each tier, in minor units with an upper-case ISO currency code.
List available features
GET /v1/projects/:projectId/hosting/shared/featuresReturns what this environment can add to a site: databases (boolean) and
extraStorage (the add-on's price, or null). Check it before sending
database or extraStorageGb on create.
Preview a charge
GET /v1/projects/:projectId/hosting/shared/preview?tier=<tier>&extraStorageGb=<gb>Returns what creating a site with these options would charge today.
Create a site
POST /v1/projects/:projectId/hosting/sharedBody:
| Field | Type | Required | Description |
|---|---|---|---|
siteName | string | yes | 3–40 characters: a lowercase letter, then lowercase letters, numbers or underscores |
tier | string | no | STARTER (default), GROWTH or SCALE |
phpVersion | string | no | 8.1, 8.2 or 8.3 (default) |
extraStorageGb | number | no | Disk on top of the tier, in blocks of 10 GB |
database | object | no | { "name": "<name>" } to create a MySQL database with the site |
The prorated amount is charged to the project's payment method. There is no
checkout redirect. A project without a payment method gets 402; see
Billing → Deploy Payment Errors.
Response: { tenantId, status }. When database was sent, the response
also carries database, or databaseError if the site was created and the
database was not. The site is kept either way; create the database again from
the databases endpoint.
Change tier
PATCH /v1/projects/:projectId/hosting/shared/:tenantId/tierBody: { "tier": "STARTER" | "GROWTH" | "SCALE" }
New PHP workers, memory, domain and database limits apply straight away. Disk size does not change.
Delete a site
DELETE /v1/projects/:projectId/hosting/shared/:tenantIdDeletes the site, its files, its databases and its attached domains. This cannot be undone.
Domains
Every hostname a site answers on, other than its own address, is a domain you
own or a subdomain of one. Each serves a folder, redirects, or is parked. The
site's own cosmoner.com address takes no subdomains.
List domains
GET /v1/projects/:projectId/hosting/shared/:tenantId/domainsResponse:
{
"success": true,
"data": [
{
"id": "cmf1…",
"tenantId": "cmf0…",
"hostname": "example.com",
"kind": "SITE",
"documentRoot": "/var/www/h_mysite_x7k2mf/example.com/public_html",
"redirectTarget": null,
"redirectStatus": null,
"status": "ACTIVE",
"routingStatus": "VERIFYING",
"createdAt": "2026-09-19T10:00:00.000Z"
}
]
}routingStatus is VERIFYING until your DNS records are in place and the
certificate is issued, then ACTIVE. It is refreshed each time the list is
read. documentRoot is null unless kind is SITE.
Attach a domain
POST /v1/projects/:projectId/hosting/shared/:tenantId/domainsBody:
| Field | Type | Required | Description |
|---|---|---|---|
hostname | string | yes | A domain you own, such as example.com or shop.example.com |
kind | string | no | SITE (default), REDIRECT or PARKED |
path | string | no | SITE only. Folder to serve, relative to the site's home |
redirectTarget | string | REDIRECT | Full http(s) address, without a query string. The requested path is appended |
redirectStatus | number | no | REDIRECT only. 301 (default) or 302 |
A field that belongs to another kind is refused with 400, not ignored.
Without path, a site is served from <hostname>/public_html. A subdomain of
a domain the site already has defaults to a folder inside that domain's own:
shop.example.com to example.com/shop. The folder is created if it
does not exist.
Response:
{
"success": true,
"data": {
"domainId": "cmf1…",
"hostname": "example.com",
"instructions": {
"recordType": "CNAME",
"name": "example.com",
"value": "…",
"ownershipVerification": { "type": "txt", "name": "…", "value": "…" }
}
}
}Add the records in instructions at your DNS provider.
Errors: 400 when the tier's domain limit is reached or the hostname is
under cosmoner.com, 409 when the hostname is already attached to any site.
Change a domain
PATCH /v1/projects/:projectId/hosting/shared/:tenantId/domains/:domainIdTakes kind, path, redirectTarget and redirectStatus as above. The body
is the domain's whole behaviour, not a partial update: { "kind": "SITE" }
alone moves the domain to its default folder.
The hostname stays attached, so its DNS records and certificate are untouched.
Detach a domain
DELETE /v1/projects/:projectId/hosting/shared/:tenantId/domains/:domainIdThe hostname stops pointing at the site. Any folder it served stays where it is, with its files.
Files
Paths are relative to the site's home folder, with no leading slash. An empty
path is the home folder itself. .. is refused.
| Method | Path | Description |
|---|---|---|
GET | /:tenantId/files?path=<folder> | List a folder |
GET | /:tenantId/files/content?path=<file> | Read a text file, up to 1 MB |
GET | /:tenantId/files/download?path=<file> | Download a file, up to 64 MB |
PUT | /:tenantId/files/content | Write a text file: { path, content }, up to 1 MB. Creates the file if needed |
POST | /:tenantId/files/upload?path=<folder>&name=<file> | Upload one file. The body is the file, sent as application/octet-stream, up to 64 MB |
POST | /:tenantId/files/folders | Create a folder: { path } |
POST | /:tenantId/files/rename | Rename or move: { from, to } |
DELETE | /:tenantId/files?path=<entry> | Delete a file, or a folder and everything in it |
Files run as the site's own user, so what the API creates is what an SFTP session sees and owns. Larger files go over SFTP.
A listing entry is { name, type, size, modifiedAt }, where type is file,
directory, symlink or other.
Databases
| Method | Path | Description |
|---|---|---|
GET | /:tenantId/databases | List the site's MySQL databases |
GET | /:tenantId/databases/:databaseId | Get one, including dbPassword |
POST | /:tenantId/databases | Create one: { name } |
DELETE | /:tenantId/databases/:databaseId | Delete the database and its user |
name is 1–16 lowercase letters and numbers, starting with a letter. The real
database and user name are returned as dbName and dbUser, alongside host,
port and maxConnections. The number of databases and the connections each
one gets depend on the tier.