API reference
A plain HTTP API. No SDK, no CLI, nothing to install — anything that can send a request can publish a website.
On this page
Base URL and authentication
Every endpoint lives under https://dashploy.com/api and takes a bearer token:
Authorization: Bearer dp_live_…
The token is the same access code the product asks you to paste into an AI assistant, and it is created for you when you sign up. You can see it on your codes page until an assistant has used it once; after that it cannot be shown again and the only option is to replace it, which takes effect immediately.
Websites are served from *.dashploy.app, a separate domain from this one, so nothing anybody publishes shares an origin with the control plane or its cookies.
Responses and errors
Every response is JSON and carries ok. A failure carries an errors array, and each error is structured rather than a string:
{
"ok": false,
"errors": [
{
"stage": "upload",
"code": "MISSING_DEPENDENCY",
"message": "The package \"express\" is not available.",
"hint": "Use hono instead — it is curated and works the same way here.",
"retryable": false
}
]
}
hint is the field to read. It is written to be acted on rather than displayed — it names the replacement, not the problem — and it is why a failure here is usually fixable in the same turn rather than becoming a support question.
retryable says whether sending the same request again could ever succeed. It is set conservatively: a wrong true costs a retry loop, so when it says false, stop and change something.
Every code is listed with a plain-language explanation on the error page. Each endpoint below names the codes it can return.
Quickstart
One request publishes a website. The project is created on first use, so there is nothing to set up first.
curl -X POST https://dashploy.com/api/v1/deploy \
-H "Authorization: Bearer $DASHPLOY_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "project": "my-site", "files": [ { "path": "index.html", "content": "<h1>Hello</h1>" } ] }'
Running that a second time against a project that already has a website is refused with 409 unless you add "replace": true. Publishing replaces everything that is there, so it has to be asked for — and nothing is lost either way, because every earlier version stays restorable.
Account
What this token can do, and what is stopping it. Call this first when anything else fails.
The account behind this token, what it is allowed to do, and what is currently blocking it.
The first call to make when anything else fails. It answers "is the token valid", "what plan is this", "what is used up" and "what must the user do about it" in one response, so a client never has to report a failure it cannot explain. Each can* flag is a plain boolean to branch on; "whatToDo" is the sentence to relay when one of them is false.
Response
{
"ok": true,
"email": "someone@example.com",
"plan": "starter",
"verification": "trusted",
"canDeployStaticSites": true,
"canDeployApps": true,
"canCreateProject": true,
"canDeployToday": true,
"canStoreNewFiles": true,
"projects": { "used": 2, "limit": 5 },
"usage": {
"requests": { "used": 8633, "limit": 1000000 },
"computeMs": { "used": 137356, "limit": 10000000 },
"percentOfPlan": 1,
"periodResets": "2026-09-01",
"fileWrites": { "usedThisMonth": 16, "limit": 200000, "remaining": 199984 },
"deploys": { "usedToday": 0, "limit": 50, "remaining": 50, "resetsAt": "2026-08-31T00:00:00.000Z" },
"storage": {
"websiteBytes": 0,
"appUploadBytes": 411438,
"usedBytes": 411438,
"accountLimitBytes": 1073741824,
"perProjectLimitBytes": 524288000
}
},
"blocked": [],
"whatToDo": ["Nothing is blocked. Deploys should work."],
"links": {
"dashboard": "https://dashploy.com/app",
"tokens": "https://dashploy.com/app/tokens",
"verify": "https://dashploy.com/app/verify"
}
}
An account with nothing blocked. When something is, "blocked" names it, "whatToDo" carries the sentence to relay, and "upgrade" appears with the page to send the user to.
Other statuses
- 401 — The token is missing, revoked, or names an account that is gone.
Projects
A project is a name, an address and a place to publish to.
Every project this account owns, with its live address.
Response
{
"ok": true,
"projects": [
{
"id": "0f3c…",
"name": "my-site",
"url": "https://my-site.dashploy.app",
"suspended": false,
"createdAt": "2026-08-01T10:14:22.000Z"
}
]
}
Whether a name is free, before offering it to somebody.
Names are global across the platform, so one that reads as obviously available often is not. Checking first turns "propose, create, 409, apologise, propose again" into a single exchange. Reserved and malformed names are refused here with the same wording the create call would use.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | yes | The name to check, without a domain. |
Response
{
"ok": true,
"available": false,
"reason": "That name is already taken.",
"name": "my-site",
"url": "https://my-site.dashploy.app"
}
A name that is taken. A free one answers the same shape without "reason" — "url" is returned either way, so you can show the user the address before they agree to it.
Other statuses
- 400 — The name is malformed or reserved.
- 409 — The name is taken.
Reserve a name without publishing anything to it yet.
Rarely needed. Deploying creates the project itself, so calling this first only spends a project slot more slowly. It exists for the case where a name has been agreed but the code is not ready.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Lowercase letters, numbers and hyphens. Becomes the public address: name.dashploy.app. |
Response
{
"ok": true,
"project": {
"id": "0f3c…",
"name": "my-site",
"url": "https://my-site.dashploy.app"
}
}
Other statuses
- 201 — The project was created and the name is now reserved.
- 403 — The plan’s project limit is reached.
- 409 — The name is taken.
Change a project’s name, and therefore its address.
The old address stops working immediately and is released for anyone to claim. No files move: a project’s bytes are stored under a prefix that does not change, which is why a rename is instant however large the site.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| name | string | yes | The new name. |
Response
{
"ok": true,
"project": { "name": "new-name", "url": "https://new-name.dashploy.app" },
"previousName": "old-name"
}
Other statuses
- 400 — The new name is malformed or reserved.
- 409 — The new name is taken.
Take a site down and remove everything held for it.
Irreversible, and it removes the files, the stored source snapshots, anything the app itself stored, and the logs. The site stops serving first and the record is removed last: if it fails in between, retry the same call — a half-finished delete answers DELETE_INCOMPLETE precisely so a client is told to retry rather than reporting success.
Response
{ "ok": true, "deleted": "my-site" }
Other statuses
- 503 — The delete began and did not finish. Retry the same call.
Publishing and undoing
Publishing replaces a site, so it has to be asked for. It deletes nothing, so it can always be undone.
Publish files to a live HTTPS address, creating the project if it does not exist.
Send every file the site needs, including index.html. A project that already has a live site refuses this with 409 unless "replace" is true — publishing replaces everything that is there, and nothing in an API call distinguishes "create a site here" from "replace the site that is here". Nothing is lost either way: content is stored by hash and every earlier version stays restorable. On failure the response carries structured errors whose "hint" says how to fix the code.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| project | string | yes | The project name. Created on first use. |
| files | array | yes | Each { "path": "index.html", "content": "…" }, with an optional "encoding": "base64" for binary files. |
| replace | boolean | no | Required, and must be true, to publish over a project that already has a live site. |
Response
{
"ok": true,
"deploymentId": "9a21…",
"url": "https://my-site.dashploy.app",
"tier": "static",
"framework": "plain",
"fileCount": 12,
"uploadedFiles": 3,
"warnings": []
}
Other statuses
- 400 — The files or the name were refused. Read every "hint" and try again.
- 403 — The account is suspended or over a limit.
- 409 — The project already has a live site and "replace" was not true.
The last twenty versions of a site, newest first.
Each carries the id that the restore call takes, plus any errors recorded at the time.
Response
{
"ok": true,
"deployments": [
{
"id": "9a21…",
"status": "live",
"url": "https://my-site.dashploy.app",
"tier": "static",
"created_at": "2026-08-28T09:02:11.000Z",
"errors": null,
"warnings": null
}
]
}
Other statuses
- 404 — No project of that name on this account.
Put an earlier version back.
Publishing never deletes anything, so this rewrites a single pointer and takes effect immediately. Any version listed by the deployments call can be restored, including one from months ago.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| deployment | string | yes | An id from the deployments list. |
Response
{
"ok": true,
"url": "https://my-site.dashploy.app",
"deploymentId": "9a21…",
"whatToDo": "The earlier version is live again."
}
Other statuses
- 400 — No deployment of that id on this project.
Environment variables
Write-only. Values can be set, listed by key and deleted, never read back.
Which variables are set, by name.
Names only. Values are never returned by any endpoint: they are stored encrypted, uploaded as secret bindings, and redacted from logs and errors. A secret that can be read back through an API is not a secret.
Response
{
"ok": true,
"env": [{ "key": "STRIPE_KEY", "updatedAt": "2026-08-20T11:00:00.000Z" }]
}
Set or replace variables. Takes effect on the next deploy.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| vars | object | yes | A flat object of name to value. Existing names are overwritten. |
Response
{
"ok": true,
"keys": ["STRIPE_KEY"],
"note": "Set. These reach the app on its next deploy."
}
Other statuses
- 400 — A name is not a valid identifier, or there are too many.
Logs
What a server-side app printed, and what went wrong.
Recent runtime logs for a server-side app, with the errors summarised first.
Static sites produce none, and an app that has not been visited since deploying produces none either — "note" says which, because an empty list otherwise has two very different meanings and guessing wrong sends a client down the wrong path.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| level | string | no | Only entries at this level, e.g. "error". |
| since | string | no | ISO timestamp; only entries after it. |
Response
{
"ok": true,
"summary": "3 errors, all TypeError: undefined is not a function",
"count": 14,
"logs": [
{ "at": "2026-08-28T09:04:00.000Z", "level": "error", "message": "TypeError: …" }
],
"note": "No runtime logs found. Either the app has not been visited since deploying, or it is a static site, which produces none."
}
"note" appears only when the list is empty, and it is the field that tells the two reasons apart.
Other statuses
- 400 — "since" is not a timestamp that can be read.
Custom domains
Connecting a domain the user already owns, and reporting on whether it works yet.
Domains connected to a project, and exactly which checks have passed.
Connecting example.com also registers www.example.com, and the pair is one connection. "live" covers the WHOLE connection, so it stays false while either half is unfinished — poll that field rather than "status". Each check carries "needsUser": false means it is happening on its own and the only correct advice is to wait; true means nothing changes until the user acts.
Response
{
"ok": true,
"cnameTarget": "fallback.dashploy.app",
"domains": [
{
"hostname": "example.com",
"live": false,
"status": "pending",
"certificate": "pending",
"alsoCovers": "www.example.com",
"hostnames": [
{
"hostname": "example.com",
"live": true,
"status": "active",
"certificate": "active",
"checks": [{ "label": "DNS record", "state": "done", "needsUser": false }]
}
],
"records": [
{ "type": "CNAME", "name": "www", "value": "fallback.dashploy.app" }
]
}
],
"nameNote": "…",
"propagationNote": "…"
}
"alsoCovers" is present only where a www companion was registered with the domain. "nameNote" and "propagationNote" are fixed sentences worth relaying to somebody entering records.
Connect a domain the user already owns.
Returns the DNS records to add. Relay them verbatim — a wrong value means the domain silently never activates. Poll the domains list afterwards and watch "live".
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| hostname | string | yes | The domain, without a scheme. |
Response
{
"ok": true,
"hostname": "example.com",
"setup": {
"records": [
{ "type": "CNAME", "name": "www", "value": "fallback.dashploy.app" }
]
}
}
Other statuses
- 401 — The token cannot reach this project.
- 409 — The domain is connected to another account.
Disconnect a domain, and its www companion with it.
Response
{ "ok": true, "removed": "example.com" }
Other statuses
- 404 — That domain is not connected to this project.
Buying a domain
Registration is final the moment a registry accepts it. Both prices are quoted together, everywhere.
Which extensions can be bought here, and roughly what each costs.
Standard prices, not a quote for a particular name — a registry can price an individual name as premium, which only a search reveals. Both the first-year and the renewal price are always returned together.
Response
{
"ok": true,
"extensions": [
{
"tld": "com",
"priceCents": 1499,
"price": "$14.99",
"renewalPriceCents": 1699,
"renewalPrice": "$16.99",
"promotional": false
}
],
"suggested": ["com", "ca", "dev"],
"restricted": ["bank", "gov"],
"note": "These are standard prices, not a quote for a particular name.",
"whatToSay": "…"
}
Check names against the registry, with the price each would actually cost.
Availability is live and is NOT a reservation — a name can be taken between this call and the purchase. Quote both prices to the user: a first-year price shown alone is the standard dark pattern in this industry.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | yes | A name, with or without an extension. |
Response
{
"ok": true,
"results": [
{
"hostname": "example.com",
"available": true,
"priceCents": 1499,
"price": "$14.99",
"renewalPriceCents": 1699,
"renewalPrice": "$16.99",
"promotional": false
},
{ "hostname": "example.net", "available": false }
],
"note": "Availability is checked against the registry right now.",
"whatToSay": "…"
}
A taken name carries only "hostname" and "available": false. An available one that could not be priced carries "unavailableReason" instead of a price.
Other statuses
- 400 — The query is not a name that could exist.
Whether registrant details are already on file.
The address itself is never returned — only whether it exists, the name, and the country. A registry legally requires a real name, address and phone number before it will register anything.
Response
{
"ok": true,
"onFile": true,
"name": "Jordan Lee",
"country": "CA",
"countryName": "Canada",
"city": "Edmonton",
"editUrl": "https://dashploy.com/app/domains/contact",
"whatToDo": "Contact details are on file for Jordan Lee in Canada…"
}
Details already on file. With none, only "onFile": false and "whatToDo" come back — everything else is absent rather than empty.
Store the registrant details a registration requires.
Stored encrypted and sent to the registry when a domain is registered. Offer the user the alternative: they can enter these at /app/domains/contact instead, which keeps their address out of the conversation history.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| firstName | string | yes | Given name. |
| lastName | string | yes | Family name. |
| string | yes | Contact address for the registry. | |
| phone | string | yes | In international format, e.g. +1.7805551234. |
| address | string | yes | Street address. |
| city | string | yes | Town or city, as the registry should record it. |
| postalCode | string | yes | Postal or ZIP code. |
| country | string | yes | Two-letter ISO country code. |
| state | string | no | Province or state, where the registry wants one. |
| company | string | no | Organisation, if registering to one. |
Response
{ "ok": true, "whatToDo": "Saved. These details will be sent to the domain registry…" }
Other statuses
- 400 — A required field is missing or malformed.
Create an order and return a checkout page for the user to pay on.
This does not register anything. It returns a Stripe checkout URL; registration happens after payment. A registration is the one thing here that cannot be undone, so tell the user both prices and get an explicit yes before calling this — "confirmed" must be true.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| hostname | string | yes | The exact name from a search result. |
| confirmed | boolean | yes | Must be true, and only after the user has agreed to both prices. |
| promoCode | string | no | A promotion code, if the user has one. |
Response
{
"ok": true,
"orderId": "5c8f…",
"hostname": "example.com",
"priceCents": 1499,
"price": "$14.99",
"renewalPriceCents": 1699,
"renewalPrice": "$16.99",
"checkoutUrl": "https://checkout.stripe.com/…",
"whatToDo": "Send the user to checkoutUrl, then poll the order."
}
Other statuses
- 400 — The name is not purchasable, or "confirmed" was not true.
- 503 — The registrar could not be reached. Retryable.
What happened to an order after the user was sent to pay.
Poll this after handing over a checkout link, or the last thing a client can say about a purchase is a URL. "registered" is the terminal success. An order can also sit at "paid" when the registrar returned an unclear answer — that is settled against the registry rather than refunded or retried, because either could cost the customer their name or their money.
Response
{
"ok": true,
"order": {
"id": "5c8f…",
"hostname": "example.com",
"status": "registered",
"price": "$14.99",
"renewalPrice": "$16.99"
},
"whatToDo": "Registered. Tell the user the domain is theirs."
}
Other statuses
- 404 — No order of that id on this account.
Domains bought here, when they renew, and what they are connected to.
Response
{
"ok": true,
"domains": [
{
"hostname": "example.com",
"expiresAt": "2027-08-14",
"autoRenew": true,
"connectedTo": "my-site"
}
]
}
Point a domain bought here at one of the account’s projects.
For a domain registered with us the DNS is set for the user, so "dnsRequired" is false and there is nothing to relay — do not hand them records they have nowhere to enter. Otherwise "setup" carries the records to add.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| project | string | yes | The project name to point at. |
Response
{
"ok": true,
"hostname": "example.com",
"project": "my-site",
"dnsRequired": false,
"whatToDo": "Done. We set the DNS ourselves because this domain is registered with us."
}
A domain registered with us, where the DNS is set for the user. For one registered elsewhere "dnsRequired" is true and "setup" carries the records to relay.
Other statuses
- 400 — No such project on this account.
- 404 — That domain is not one of this account’s.
Turn automatic renewal on or off for a domain bought here.
Renewals are charged against the registry’s own expiry date, thirty days out, with retries. Switched off, the domain lapses at expiry and that cannot be reversed once a registry has released it.
Request body
| Name | Type | Required | Description |
|---|---|---|---|
| enabled | boolean | yes | Whether to renew automatically. |
Response
{
"ok": true,
"autoRenew": true,
"whatToDo": "Auto-renew is on.",
"means": "We will charge the card on file 30 days before it expires."
}
Other statuses
- 404 — That domain is not one of this account’s.
Rate and size limits
Allowances are per plan and per calendar month, except deploys, which reset at midnight UTC. Requests means individual files fetched by a browser, not page views.
| Plan | Projects | Deploys a day | Requests a month | Storage a project |
|---|---|---|---|---|
| Free | 2 | 10 | 100,000 | 52 MB |
| Starter | 5 | 50 | 1,000,000 | 524 MB |
| Pro | 10 | 200 | 10,000,000 | 2,684 MB |
Going over does not delete anything. A free account's websites stop being served until the period resets; a paid account is given a grace band above its allowance where sites are still served but more slowly. GET /api/v1/whoami reports what is used and what is blocked, which is why it is the first call to make when something fails.
MCP server
Every endpoint above is also an MCP tool, named beside it. The tools are not a separate product and not a subset: each one makes an internal request to the same handler, so authentication, quotas and error shapes cannot diverge between the two.
The server is at https://dashploy.com/mcp, over Streamable HTTP, with the same Authorization: Bearer header. Most clients ask for both when you add a server.
If you are wiring up an assistant rather than writing code yourself, /llms.txt is written for it to read directly — same endpoints, with the guidance a model needs about when to call which.