Sign in

Agency API — full reference

Updated Sep 1, 2026

Deploy and manage hosted sites programmatically — create sites, attach custom domains, issue certificates, and manage storage over plain HTTP. Available on the Whitelabel Agency plan.

Authentication

Every request is authenticated with your account API key, sent as a header:

X-API-Key: dvh_your_key_here

Generate or rotate the key in your dashboard under Account → API access. The raw key is shown only once at creation — store it securely. API access requires a Whitelabel Agency plan.

  • Missing/invalid key → 401
  • Plan without API access → 403

Base URL for every path below:

https://app.devani.io/api/v1

Conventions

  • Requests and responses are JSON. Send Content-Type: application/json on POST/PATCH.
  • Errors are { "detail": "message" } with a non-2xx status (validation 400/422, auth 401, forbidden 403, not found 404).
  • Site IDs are integers. Timestamps are UTC.
  • Plan limits (site count, custom domains, storage) are enforced automatically — exceeding one returns 400 with a message.
  • POST /sites blocks until the site is provisioned and serving (~40s), so the response is the live site.

Account

GET /account — Your plan, effective limits, current usage, and the hub MCP connection URL.

{
  "email": "you@agency.com",
  "plan": { "key": "whitelabel", "name": "Whitelabel Agency" },
  "limits": { "sites": 35, "staging": 10, "cdn_mb": 307200, "custom_domains": 35 },
  "usage":  { "sites": 4, "staging": 0, "cdn_allocated_mb": 20480 },
  "hub_mcp_url": "https://app.devani.io/api/mcp"
}

Sites

GET /sites — List every site on your account.

POST /sites — Deploy a new site. Returns when it's live on its free *.devani.studio address.

  • slug — string, required — the subdomain label (slug.devani.studio), 3–40 chars.
  • display_name — string, optional — friendly name.
  • custom_domain — string, optional — attach a custom domain at creation (DNS auto-configured if you've connected Cloudflare; otherwise records are returned to add).
  • staging — bool, optional — provision a staging site (counts against the staging limit, free subdomain only).
curl -X POST https://app.devani.io/api/v1/sites \
  -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"slug":"acme","display_name":"Acme Co"}'

The response is a Site object — including devani_admin with the site's CMS login and its per-site MCP URL.

Retries are safe (idempotent). If a create is interrupted and you retry the same slug, you get the existing site back (201) instead of an error. Note: the one-time CMS password is only shown on the first successful create; a retry won't repeat it (regenerate from the dashboard if lost). A custom domain already held by one of your own sites returns a clear message naming that site.

GET /sites/{id} — Full detail for one site, including devani_admin (CMS login + MCP URL) and any pending custom-domain setup.

DELETE /sites/{id} — Permanently tear a site down (files, database, vhost, certificate, and any custom-hostname registration).

Custom domains

POST /sites/{id}/domains — Attach a custom domain. If you've connected your Cloudflare token (Whitelabel auto-DNS), the records are created for you; otherwise the records to add are returned in the site's custom_domain_setup. The domain goes live on HTTPS automatically once it validates.

  • domain — string, required — e.g. www.client.com.
  • replace_dns — bool, optional — if a conflicting DNS record already exists, replace it (a live-domain cutover). Default false surfaces the conflict instead.

POST /sites/{id}/domains/{domain_id}/dns?replace=true — (Re)run the automatic DNS for an attached domain — use replace=true to confirm replacing a conflicting record.

DELETE /sites/{id}/domains/{domain_id} — Detach a custom domain.

A site keeps its free *.devani.studio address as the instant URL. Once a custom domain's certificate is active it becomes the primary, and the free subdomain automatically 301-redirects to it.

Certificates

POST /sites/{id}/ssl — Issue or re-issue the certificate (e.g. after pointing a custom domain's DNS). Idempotent. Returns the updated Site.

Storage

PATCH /sites/{id}/storage-limit — Set this site's managed-storage (Devani CDN) cap. The account-wide sum of caps can't exceed your plan allowance.

  • limit_mb — integer — per-site cap in MB.

Upload file types

PATCH /sites/{id}/upload-filetypes — Choose which file types the site's media uploader accepts. Images and video are always allowed; pass a list of extra extensions to also allow them, or an empty list to return to images+video only.

  • extensions — string[] — extra extensions, no dot (e.g. ["rfa","dwg","pdf"]). Max 40. Script/markup types (php, html, svg, js, …) are rejected for security.

Devani version & updates

GET /sites/{id}/devani — The Devani version this site runs, whether its release feed has something newer, and the backups the install holds (pre-update code backups, daily content snapshots, hourly storage snapshots).

POST /sites/{id}/devani/update — Update the site to the latest Devani release. The install's own updater runs: it backs up the current code first and preserves all content, config, and snapshots. A no-op (updated: false) if already current. Response includes previous_version, version, and the backup_name it created.

SFTP

POST /sites/{id}/sftp?enabled=true — Enable chrooted SFTP (the password is returned once) or disable it (enabled=false). Re-enabling rotates the password.

Cloudflare DNS

If you've connected a Cloudflare token (Account → Automatic DNS), these routes let you read and manage the DNS on your own Cloudflare account through the same API you use for everything else — so a provisioning script never has to hold a Cloudflare key. The token stays server-side and is never returned; you authenticate with your X-API-Key only.

GET /cloudflare/zones — List the zones (domains) on your Cloudflare account. Optional ?name= filter.

{ "zones": [ { "id": "abc123", "name": "client.com", "status": "active",
              "paused": false, "name_servers": ["..."] } ] }

GET /cloudflare/zones/{zone_id}/dns — List DNS records in a zone. Optional ?type= (A, CNAME, TXT…) and ?name= filters. Returns Cloudflare's record objects (id, type, name, content, proxied, ttl).

POST /cloudflare/zones/{zone_id}/dns — Create a DNS record.

  • type — string, required — A, AAAA, CNAME, TXT, MX, …
  • name — string, required — record name (e.g. www.client.com).
  • content — string, required — the value (IP, target, text).
  • proxied — bool, optional (A/AAAA/CNAME) — default false.
  • ttl — integer, optional — 1 = automatic.
  • priority — integer, optional (MX/SRV).

PATCH /cloudflare/zones/{zone_id}/dns/{record_id} — Update a record — send only the fields you want to change.

DELETE /cloudflare/zones/{zone_id}/dns/{record_id} — Delete a record by id.

Requires a connected Cloudflare token (Whitelabel Agency). These routes act only on your account's Cloudflare, scoped by whatever that token allows.

The full loop

  1. POST /sites → the site is live on its free address.
  2. It immediately appears in your hub MCP (hub_mcp_url) — point Claude at it and build it out.
  3. POST /sites/{id}/domains → attach the client's domain; with Cloudflare connected it auto-configures, otherwise add the returned records.
  4. The domain validates, the certificate issues, and it becomes primary — the free subdomain 301s to it.

Object shapes

Site

{
  "id": 12,
  "slug": "acme",
  "display_name": "Acme Co",
  "primary_domain": "www.client.com",
  "url": "https://www.client.com",
  "site_status": "active",
  "has_ssl": true,
  "setup_pending": false,
  "domains": [ { "id": 31, "name": "acme.devani.studio", "is_primary": false, "ssl": true },
               { "id": 32, "name": "www.client.com", "is_primary": true, "ssl": true } ],
  "devani_admin": {
    "url": "https://www.client.com/devani/",
    "username": "owner",
    "password": "shown once at creation",
    "mcp_url": "https://www.client.com/devani/mcp/?api_key=..."
  },
  "custom_domain_setup": []
}
Was this helpful?

Related articles

Powered by Subido