On this page
Guides / Static sites

Static sites

Publish an already built site —HTML, CSS, JS and images— on your metsite.co subdomain, with versions and one-step rollback.

Updated View .md

If your site is already built —by hand, with a generator like Astro, Vite or Hugo, or with an assistant—, you upload it as is and Meteor hosts it at https://<slug>.metsite.co. Each upload is a version that does not change; rolling back means putting an earlier version online, without uploading anything again.

There are three steps: create the site once → upload a version every time it changes → activate it (by default it goes live on its own when uploaded).

Before you start

ScopeWhat forWith a test key
sites:writeCreate the site, upload versions and activate them.No: it responds 403 test_mode_restricted, because it publishes on the internet.
sites:readList the versions.Yes

Routes are flat (/sites/…): they operate on the key's workspace, and a site from another workspace responds 404.

1. Create the site

const site = await met.sites.createStatic({ name: 'Neuroliderazgo', slug: 'neuroliderazgo' });
// { site_id: 812, slug: 'neuroliderazgo', url: 'https://neuroliderazgo.metsite.co' }
site = met.sites.create_static(name="Neuroliderazgo", slug="neuroliderazgo")
curl -X POST https://api.met.meteor.com.co/api/v1/sites/static \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Neuroliderazgo","slug":"neuroliderazgo"}'

2. Upload a version

From a zip, the simplest way when you already have the built folder (up to 50 MB):

import { readFile } from 'node:fs/promises';

const zip = await readFile('site.zip');
const version = await met.sites.deployZip(site.site_id, zip);
// { id: '3f6c1e0a-…', status: 'ready', source: 'zip', file_count: 42, total_bytes: 5242880, is_active: true, url: 'https://neuroliderazgo.metsite.co' }
with open("site.zip", "rb") as f:
    version = met.sites.deploy_zip(site["site_id"], f.read())
cd dist && zip -r ../site.zip . && cd ..
curl -X POST https://api.met.meteor.com.co/api/v1/sites/812/deploys \
  -H "Authorization: Bearer $METEOR_API_KEY" \
  -F file=@site.zip

Or file by file, as JSON: content in utf8 for text and in base64 for binaries. The whole body has a 10 MB cap; a bigger site goes by zip.

import { readdir, readFile } from 'node:fs/promises';
import { join, relative, extname } from 'node:path';

const TEXT = new Set(['.html', '.css', '.js', '.mjs', '.json', '.svg', '.txt', '.xml', '.webmanifest', '.map']);

async function filesIn(folder: string, root = folder) {
  const out: { path: string; content: string; encoding: 'utf8' | 'base64' }[] = [];
  for (const entry of await readdir(folder, { withFileTypes: true })) {
    const path = join(folder, entry.name);
    if (entry.isDirectory()) out.push(...(await filesIn(path, root)));
    else {
      const isText = TEXT.has(extname(entry.name));
      const bytes = await readFile(path);
      out.push({
        path: relative(root, path).split('\\').join('/'),
        content: isText ? bytes.toString('utf8') : bytes.toString('base64'),
        encoding: isText ? 'utf8' : 'base64',
      });
    }
  }
  return out;
}

const version = await met.sites.deployFiles(812, { files: await filesIn('dist') });
import base64, pathlib

TEXT = {".html", ".css", ".js", ".mjs", ".json", ".svg", ".txt", ".xml", ".webmanifest", ".map"}
root = pathlib.Path("dist")
files = []
for p in root.rglob("*"):
    if p.is_file():
        is_text = p.suffix in TEXT
        files.append({
            "path": p.relative_to(root).as_posix(),
            "content": p.read_text("utf-8") if is_text else base64.b64encode(p.read_bytes()).decode(),
            "encoding": "utf8" if is_text else "base64",
        })

version = met.sites.deploy_files(812, files=files)

Each call is a complete version: files are not added to the previous one. With activate: false (activate=False in Python) the version is uploaded without going online, so you can review it or publish it later.

What is accepted

RuleCap
Zip50 MB
Files per version2000
One file, uncompressed25 MB
The whole version, uncompressed200 MB
deployFiles JSON10 MB
Length of a path512 characters

How routes are served

Versions and rollback

const versions = await met.sites.listDeploys(812, { limit: 10 });
// newest to oldest; is_active: true on the one that is online

const previous = versions.find((v) => !v.is_active && v.status === 'ready');
if (previous) await met.sites.activate(812, previous.id);
versions = met.sites.list_deploys(812, limit=10)
previous = next((v for v in versions if not v["is_active"] and v["status"] == "ready"), None)
if previous:
    met.sites.activate(812, previous["id"])
curl -X POST https://api.met.meteor.com.co/api/v1/sites/812/deploys/3f6c1e0a-5b7d-4c2e-9a1f-0d8b7e6a5c4b/activate \
  -H "Authorization: Bearer $METEOR_API_KEY"
statusWhat it is
readyUpload complete: it can be activated.
uploadingStill uploading.
failedThe upload did not finish; error says why. Upload another version.

listDeploys returns 20 by default (from 1 to 100). activate only puts online a ready version of that site; the first activation publishes the site. The change is immediate and nothing is uploaded again.

Your domain

The site lives at https://<slug>.metsite.co. Connecting a custom domain (www.yourbusiness.com) is not done through the API.

From an assistant (MCP)

These operations are reachable with the MCP server's generic tool (met_call_operation), in their file-by-file form: the zip does not travel over MCP, because an assistant does not attach binaries in the conversation.

Common errors

CodeWhat happened
400 validation_errorThe slug does not match the format, or it is reserved.
400 deploy_files_rejectedSome path or extension is not accepted; check errors. Nothing was uploaded.
400 deploy_zip_missingThe zip did not arrive in the multipart file field.
400 zip_invalidThe file is not a valid zip.
400 deploy_empty · deploy_too_many_files · deploy_too_large · deploy_zip_too_largeThe version has no files or exceeds one of the caps.
413 FILE_TOO_LARGEThe zip uploaded via multipart exceeds 50 MB.
403 test_mode_restrictedA met_test_ key does not publish sites.
404 not_found · deploy_not_foundThe site does not belong to this workspace, or the version does not belong to this site.
409 conflictThe slug is already in use.
409 site_not_staticThe site is built with blocks in the panel: versions are only for sites created with createStatic.
409 deploy_not_readyTried to activate a version that is not ready.
503 deploy_upload_failedThe upload failed on our side. The version stays failed and the one that was online stays online: upload it again.