DashPloy
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.

What a token can do
A token carries the whole account. It can publish to, rename and delete every website on it, so treat it as a password: keep it out of client-side code and out of anything public. Revoking one from the codes page takes effect on the next request.

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
    }
  ]
}
Reading a failure

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.

GET /api/v1/whoami #

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.
Errors INVALID_TOKEN
MCP tool whoami

Projects

A project is a name, an address and a place to publish to.

GET /api/v1/projects #

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"
    }
  ]
}
MCP tool list_projects
GET /api/v1/projects/available #

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

Query parameters
Name Type Required Description
namestringyesThe 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.
MCP tool check_project_name
POST /api/v1/projects #

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

Request body
Name Type Required Description
namestringyesLowercase 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.
MCP tool create_project
POST /api/v1/projects/<slug>/rename #

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

Request body
Name Type Required Description
namestringyesThe 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.
DELETE /api/v1/projects/<slug> #

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.
MCP tool delete_project

Publishing and undoing

Publishing replaces a site, so it has to be asked for. It deletes nothing, so it can always be undone.

POST /api/v1/deploy #

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

Request body
Name Type Required Description
projectstringyesThe project name. Created on first use.
filesarrayyesEach { "path": "index.html", "content": "…" }, with an optional "encoding": "base64" for binary files.
replacebooleannoRequired, 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.
GET /api/v1/projects/<slug>/deployments #

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.
Errors NOT_FOUND
MCP tool get_deployments
POST /api/v1/projects/<slug>/restore #

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

Request body
Name Type Required Description
deploymentstringyesAn 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.
Errors NOT_FOUND
MCP tool restore_deployment

Environment variables

Write-only. Values can be set, listed by key and deleted, never read back.

GET /api/v1/projects/<slug>/env #

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" }]
}
Errors NOT_FOUND
MCP tool list_env
PUT /api/v1/projects/<slug>/env #

Set or replace variables. Takes effect on the next deploy.

Request body

Request body
Name Type Required Description
varsobjectyesA 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.
MCP tool set_env
DELETE /api/v1/projects/<slug>/env/<key> #

Remove one variable. It stops reaching the app on the next deploy.

Response

{ "ok": true, "note": "Removed. The change reaches the app on its next deploy." }
Errors NOT_FOUND
MCP tool delete_env

Logs

What a server-side app printed, and what went wrong.

GET /api/v1/projects/<slug>/logs #

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

Query parameters
Name Type Required Description
levelstringnoOnly entries at this level, e.g. "error".
sincestringnoISO 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.
Errors NOT_FOUND
MCP tool get_logs

Custom domains

Connecting a domain the user already owns, and reporting on whether it works yet.

GET /api/v1/projects/<slug>/domains #

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.

Errors NOT_FOUND
MCP tool list_domains
POST /api/v1/projects/<slug>/domains #

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

Request body
Name Type Required Description
hostnamestringyesThe 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.
DELETE /api/v1/projects/<slug>/domains/<hostname> #

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.
MCP tool delete_domain

Buying a domain

Registration is final the moment a registry accepts it. Both prices are quoted together, everywhere.

GET /api/v1/domains/extensions #

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": "…"
}
MCP tool list_domain_extensions
GET /api/v1/domains/search #

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

Query parameters
Name Type Required Description
qstringyesA 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.
Errors INVALID_NAME
MCP tool search_domains
GET /api/v1/domains/contact #

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.

MCP tool get_domain_contact
POST /api/v1/domains/contact #

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

Request body
Name Type Required Description
firstNamestringyesGiven name.
lastNamestringyesFamily name.
emailstringyesContact address for the registry.
phonestringyesIn international format, e.g. +1.7805551234.
addressstringyesStreet address.
citystringyesTown or city, as the registry should record it.
postalCodestringyesPostal or ZIP code.
countrystringyesTwo-letter ISO country code.
statestringnoProvince or state, where the registry wants one.
companystringnoOrganisation, 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.
Errors INVALID_NAME
MCP tool set_domain_contact
POST /api/v1/domains/orders #

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

Request body
Name Type Required Description
hostnamestringyesThe exact name from a search result.
confirmedbooleanyesMust be true, and only after the user has agreed to both prices.
promoCodestringnoA 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.
MCP tool buy_domain
GET /api/v1/domains/orders/<id> #

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.
Errors NOT_FOUND
MCP tool get_domain_order
GET /api/v1/domains #

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"
    }
  ]
}
MCP tool list_registered_domains
POST /api/v1/domains/<hostname>/connect #

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

Request body
Name Type Required Description
projectstringyesThe 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.
MCP tool connect_domain
POST /api/v1/domains/<hostname>/auto-renew #

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

Request body
Name Type Required Description
enabledbooleanyesWhether 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.
Errors NOT_FOUND
MCP tool set_domain_auto_renew

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.

What each plan allows
Plan Projects Deploys a day Requests a month Storage a project
Free210100,00052 MB
Starter5501,000,000524 MB
Pro1020010,000,0002,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.