# Supadrop API

> REST API to deploy and manage static websites. Upload a file, get a live URL in seconds.

Base URL: `https://app.supadrop.host/api/v1`

## Authentication

All endpoints require a Bearer token in the `Authorization` header.

```
Authorization: Bearer sk_live_YOUR_API_KEY
```

API keys are available on Pro ($15/mo) and Large ($30/mo) plans. Generate one from Account Settings > API Access in the dashboard at https://app.supadrop.host. Each account can have one active key. Keys follow the format `sk_live_` + 64 hex characters (72 chars total). The key is shown only once at creation.

> **Security:** Store your API key in an environment variable (e.g. `SUPADROP_API_KEY`) and never commit it to version control. Add it to your `.env` file and make sure `.env` is in your `.gitignore`.

## Endpoints

### List sites

```
GET /api/v1/sites
```

Returns all sites owned by the authenticated user.

Response (200):

```json
{
  "sites": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "slug": "my-site",
      "name": "My Site",
      "file_name": "site.zip",
      "file_size": 102400,
      "status": "DEPLOYED",
      "created_at": "2026-06-17T10:00:00.000Z",
      "deployed_at": "2026-06-17T10:00:01.000Z",
      "url": "https://my-site.supadrop.site"
    }
  ]
}
```

Example:

```bash
curl https://app.supadrop.host/api/v1/sites \
  -H "Authorization: Bearer $SUPADROP_API_KEY"
```

---

### Get site

```
GET /api/v1/sites/{slug}
```

Returns a single site by its slug.

| Parameter | In   | Required | Description |
|-----------|------|----------|-------------|
| slug      | path | yes      | The site slug |

Response (200):

```json
{
  "site": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "slug": "my-site",
    "name": "My Site",
    "file_name": "site.zip",
    "file_size": 102400,
    "status": "DEPLOYED",
    "created_at": "2026-06-17T10:00:00.000Z",
    "deployed_at": "2026-06-17T10:00:01.000Z",
    "url": "https://my-site.supadrop.site"
  }
}
```

Errors:

| Status | Message |
|--------|---------|
| 404    | Site not found |

Example:

```bash
curl https://app.supadrop.host/api/v1/sites/my-site \
  -H "Authorization: Bearer $SUPADROP_API_KEY"
```

---

### Deploy site

```
POST /api/v1/sites
```

Upload a file to create a new site. Send as multipart/form-data.

| Field | In   | Required | Description |
|-------|------|----------|-------------|
| file  | form | yes      | The file to deploy (ZIP, HTML, PDF, or image) |
| slug  | form | no       | Custom subdomain slug. Auto-generated from filename if omitted. |
| name  | form | no       | Display name. Defaults to humanized slug. |

Accepted file types:

| MIME type | Type |
|-----------|------|
| application/zip, application/x-zip-compressed | zip |
| text/html | html |
| application/pdf | pdf |
| image/jpeg, image/png, image/webp, image/gif, image/svg+xml | image |

Slug rules (when provided):
- 3 to 63 characters
- Lowercase letters, numbers, and hyphens only
- Must start and end with a letter or number
- Some slugs are reserved (api, admin, www, etc.)

If slug is omitted, it is auto-generated from the filename (e.g. `portfolio.zip` becomes `portfolio`). If taken, a suffix is appended (`portfolio-2`, `portfolio-3`, etc.).

Response (201):

```json
{
  "site": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "slug": "my-portfolio",
    "name": "My Portfolio",
    "file_name": "portfolio.zip",
    "file_size": 245000,
    "status": "DEPLOYED",
    "created_at": "2026-06-17T10:00:00.000Z",
    "deployed_at": "2026-06-17T10:00:01.000Z",
    "url": "https://my-portfolio.supadrop.site"
  }
}
```

The `status` field may be `PROCESSING` if the file is queued for async processing. Poll `GET /api/v1/sites/{slug}` until it becomes `DEPLOYED`.

Errors:

| Status | Message |
|--------|---------|
| 400    | Multipart form data required |
| 400    | File is required |
| 400    | Unsupported file type |
| 400    | Slug validation error (format, reserved, or banned) |
| 403    | Site limit reached |
| 403    | Storage limit exceeded |
| 409    | Slug already taken |

Example:

```bash
# Deploy a ZIP archive with a custom slug
curl -X POST https://app.supadrop.host/api/v1/sites \
  -H "Authorization: Bearer $SUPADROP_API_KEY" \
  -F "file=@./dist.zip" \
  -F "slug=my-portfolio"

# Deploy a single HTML file (slug auto-generated)
curl -X POST https://app.supadrop.host/api/v1/sites \
  -H "Authorization: Bearer $SUPADROP_API_KEY" \
  -F "file=@./index.html"
```

---

### Update site

```
PUT /api/v1/sites/{slug}
```

Replace all files of an existing site. The slug and URL stay the same.

| Parameter | In   | Required | Description |
|-----------|------|----------|-------------|
| slug      | path | yes      | The site slug |
| file      | form | yes      | The new file to deploy |

Response (200):

```json
{
  "site": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "slug": "my-portfolio",
    "name": "My Portfolio",
    "file_name": "dist-v2.zip",
    "file_size": 204800,
    "status": "DEPLOYED",
    "created_at": "2026-06-17T10:00:00.000Z",
    "deployed_at": "2026-06-17T10:05:00.000Z",
    "url": "https://my-portfolio.supadrop.site"
  }
}
```

Errors:

| Status | Message |
|--------|---------|
| 400    | File is required |
| 403    | Storage limit exceeded |
| 404    | Site not found |

Example:

```bash
curl -X PUT https://app.supadrop.host/api/v1/sites/my-portfolio \
  -H "Authorization: Bearer $SUPADROP_API_KEY" \
  -F "file=@./dist-v2.zip"
```

---

### Delete site

```
DELETE /api/v1/sites/{slug}
```

Permanently delete a site. Removes all files and frees the slug.

| Parameter | In   | Required | Description |
|-----------|------|----------|-------------|
| slug      | path | yes      | The site slug |

Response (200):

```json
{
  "deleted": true,
  "slug": "my-portfolio"
}
```

Errors:

| Status | Message |
|--------|---------|
| 404    | Site not found |

Example:

```bash
curl -X DELETE https://app.supadrop.host/api/v1/sites/my-portfolio \
  -H "Authorization: Bearer $SUPADROP_API_KEY"
```

## Site status values

| Status      | Meaning |
|-------------|---------|
| PROCESSING  | File queued, not yet live |
| DEPLOYED    | Site is live |
| SUSPENDED   | Content flagged by automated scanner |
| FAILED      | Processing error |

When status is `PROCESSING`, poll `GET /api/v1/sites/{slug}` every 2-3 seconds until it becomes `DEPLOYED`.

## Rate limits

60 requests per minute per API key. Response headers on every request:

| Header                  | Description |
|-------------------------|-------------|
| X-RateLimit-Limit       | Max requests per window (60) |
| X-RateLimit-Remaining   | Requests left in current window |
| X-RateLimit-Reset       | Unix timestamp when the window resets |

Status 429 is returned when the limit is exceeded. Wait until `X-RateLimit-Reset` before retrying.

## Tier limits

| Tier  | Max sites | Max storage | API access | Price     |
|-------|-----------|-------------|------------|-----------|
| Free  | 1         | 50 MB       | No         | Free      |
| Starter | 2       | 500 MB      | No         | $7/mo     |
| Pro   | 10        | 5 GB        | Yes        | $15/mo    |
| Large | 30        | 20 GB       | Yes        | $30/mo    |

When a limit is reached, the API returns 403 with "Site limit reached" or "Storage limit exceeded".

## Error format

All errors return JSON:

```json
{
  "statusCode": 400,
  "statusMessage": "Human-readable error message"
}
```

Common status codes:

| Status | Meaning |
|--------|---------|
| 400    | Bad request (invalid input, missing fields) |
| 401    | Authentication failed (missing/invalid API key) |
| 403    | Forbidden (tier limit, suspended account) |
| 404    | Resource not found |
| 409    | Conflict (slug already taken) |
| 422    | Subscription expired |
| 429    | Rate limit exceeded |
| 500    | Server error |

## Workflows

### Deploy a site from a build directory

```bash
# 1. Zip your build output
cd my-project && zip -r dist.zip dist/

# 2. Deploy
curl -X POST https://app.supadrop.host/api/v1/sites \
  -H "Authorization: Bearer $SUPADROP_API_KEY" \
  -F "file=@./dist.zip" \
  -F "slug=my-app"

# 3. Site is live at https://my-app.supadrop.site
```

### Deploy and poll until live

```bash
# Deploy
RESPONSE=$(curl -s -X POST https://app.supadrop.host/api/v1/sites \
  -H "Authorization: Bearer $SUPADROP_API_KEY" \
  -F "file=@./site.zip" \
  -F "slug=demo")

SLUG=$(echo $RESPONSE | jq -r '.site.slug')
STATUS=$(echo $RESPONSE | jq -r '.site.status')

# Poll until deployed
while [ "$STATUS" = "PROCESSING" ]; do
  sleep 3
  STATUS=$(curl -s https://app.supadrop.host/api/v1/sites/$SLUG \
    -H "Authorization: Bearer $SUPADROP_API_KEY" | jq -r '.site.status')
done

echo "Site live at https://$SLUG.supadrop.site"
```

### Update an existing site

```bash
curl -X PUT https://app.supadrop.host/api/v1/sites/my-app \
  -H "Authorization: Bearer $SUPADROP_API_KEY" \
  -F "file=@./dist-v2.zip"
```

### CI/CD integration (GitHub Actions)

```yaml
- name: Deploy to Supadrop
  run: |
    zip -r dist.zip dist/
    curl -X POST https://app.supadrop.host/api/v1/sites \
      -H "Authorization: Bearer ${{ secrets.SUPADROP_API_KEY }}" \
      -F "file=@./dist.zip" \
      -F "slug=my-app"
```

## Code examples

### JavaScript (Node.js)

```javascript
const fs = require('fs');

const API_KEY = process.env.SUPADROP_API_KEY;
const BASE_URL = 'https://app.supadrop.host/api/v1';

// Deploy a site
async function deploySite(filePath, slug) {
  const form = new FormData();
  form.append('file', new Blob([fs.readFileSync(filePath)]));
  if (slug) form.append('slug', slug);

  const res = await fetch(`${BASE_URL}/sites`, {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${API_KEY}` },
    body: form,
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`${err.statusCode}: ${err.statusMessage}`);
  }

  return res.json();
}

// List all sites
async function listSites() {
  const res = await fetch(`${BASE_URL}/sites`, {
    headers: { 'Authorization': `Bearer ${API_KEY}` },
  });
  return res.json();
}

// Delete a site
async function deleteSite(slug) {
  const res = await fetch(`${BASE_URL}/sites/${slug}`, {
    method: 'DELETE',
    headers: { 'Authorization': `Bearer ${API_KEY}` },
  });
  return res.json();
}
```

### Python

```python
import os
import requests

API_KEY = os.environ["SUPADROP_API_KEY"]
BASE_URL = "https://app.supadrop.host/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}

# Deploy a site
def deploy_site(file_path, slug=None):
    files = {"file": open(file_path, "rb")}
    data = {"slug": slug} if slug else {}
    r = requests.post(f"{BASE_URL}/sites", headers=HEADERS, files=files, data=data)
    r.raise_for_status()
    return r.json()

# List all sites
def list_sites():
    r = requests.get(f"{BASE_URL}/sites", headers=HEADERS)
    r.raise_for_status()
    return r.json()

# Get a site
def get_site(slug):
    r = requests.get(f"{BASE_URL}/sites/{slug}", headers=HEADERS)
    r.raise_for_status()
    return r.json()

# Update a site
def update_site(slug, file_path):
    files = {"file": open(file_path, "rb")}
    r = requests.put(f"{BASE_URL}/sites/{slug}", headers=HEADERS, files=files)
    r.raise_for_status()
    return r.json()

# Delete a site
def delete_site(slug):
    r = requests.delete(f"{BASE_URL}/sites/{slug}", headers=HEADERS)
    r.raise_for_status()
    return r.json()
```
