# REST API Reference

Base URL: `https://curlyflies.com/v1`

## Authentication

All requests require a Bearer token:

```
Authorization: Bearer curly_live_abc123
```

- Keys are issued from the [dashboard](https://curlyflies.com/dashboard) or via [agent signup](#agent-signup).
- Keys are only ever accepted in the `Authorization` header — never in URL parameters.
- A missing or invalid key returns `401`.

## Endpoints

| Method | Path | Purpose |
|---|---|---|
| POST | `/v1/upload` | Upload a file directly |
| POST | `/v1/upload-url` | Re-host a file from a remote URL |
| GET | `/v1/files/{file_id}` | Get file metadata |
| DELETE | `/v1/files/{file_id}` | Delete a file before expiry |
| POST | `/v1/agent/signup` | Agent self-provisioning |
| GET | `/v1/usage` | Usage stats for the current key |

---

## Upload a file

`POST /v1/upload`

Request: `multipart/form-data`

| Field | Type | Required | Notes |
|---|---|---|---|
| `file` | binary | yes* | *Required if `content_base64` not provided |
| `content_base64` | string | yes* | *Required if `file` not provided |
| `filename` | string | no | Stored in metadata only; never used as the storage key |
| `content_type` | string | no | Auto-detected from magic bytes if omitted |
| `ttl_seconds` | integer | no | Default `86400` (24h). Max varies by plan |

```bash
curl -X POST https://curlyflies.com/v1/upload \
  -H "Authorization: Bearer $CURLY_KEY" \
  -F "file=@output.png"
```

Response `200`:

```json
{
  "file_id": "x7k2p9",
  "url": "https://curlyflies.com/f/x7k2p9.png",
  "expires_at": "2026-06-02T09:41:00Z",
  "size_bytes": 204800,
  "content_type": "image/png",
  "delete_token": "dt_abc123"
}
```

Notes:

- `url` is a clean public URL — no auth, no redirects, correct `Content-Type`. It passes Instagram's URL validation.
- `expires_at` is always present in any response that contains a URL. Plan around it.
- `delete_token` allows deletion without your API key (useful for handing cleanup to another agent).
- Uploads are validated by magic bytes against the declared content type. Mismatches are rejected. Images are re-encoded server-side (EXIF stripped).

Errors:

| Status | Meaning |
|---|---|
| `401` | Invalid or missing API key |
| `413` | File exceeds plan size limit (5MB free / 250MB Builder / 1GB Pro / 5GB Scale) |
| `415` | File type not allowed or magic bytes don't match declared `content_type` |
| `429` | Rate limit exceeded — check the `Retry-After` header |

---

## Upload from URL

`POST /v1/upload-url`

Fetches a remote file server-side and re-hosts it on CurlyFlies. Use this for GPT Image (OpenAI), Nano Banana (Google), Seedream, Flux, Midjourney, Replicate, Higgsfield, or any source with expiring signed URLs, auth requirements, or redirects that downstream APIs can't follow.

Request: `application/json`

| Field | Type | Required | Notes |
|---|---|---|---|
| `url` | string | yes | The remote file to fetch |
| `filename` | string | no | Metadata only |
| `ttl_seconds` | integer | no | Default `86400` |

```bash
curl -X POST https://curlyflies.com/v1/upload-url \
  -H "Authorization: Bearer $CURLY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://oaidalleapiprodscus.blob.core.windows.net/private/generated/abc123.png?se=...",
    "filename": "output.png",
    "ttl_seconds": 86400
  }'
```

Response `200`: identical shape to [`POST /v1/upload`](#upload-a-file).

---

## Get file info

`GET /v1/files/{file_id}`

Check whether a file is still live and fetch its metadata. Call this before passing a URL downstream if time has passed since upload.

```bash
curl https://curlyflies.com/v1/files/x7k2p9 \
  -H "Authorization: Bearer $CURLY_KEY"
```

Response `200`:

```json
{
  "file_id": "x7k2p9",
  "url": "https://curlyflies.com/f/x7k2p9.png",
  "expires_at": "2026-06-02T09:41:00Z",
  "size_bytes": 204800,
  "content_type": "image/png",
  "exists": true
}
```

Response `404` (expired or never existed):

```json
{
  "error": "file_not_found",
  "message": "File x7k2p9 does not exist or has expired."
}
```

---

## Delete file

`DELETE /v1/files/{file_id}`

Delete a file before it expires naturally. Authenticate with **either**:

- your normal Bearer key, **or**
- the `delete_token` from the upload response, sent as a header: `X-Delete-Token: dt_abc123`

```bash
# with Bearer auth
curl -X DELETE https://curlyflies.com/v1/files/x7k2p9 \
  -H "Authorization: Bearer $CURLY_KEY"

# with delete token (no API key needed)
curl -X DELETE https://curlyflies.com/v1/files/x7k2p9 \
  -H "X-Delete-Token: dt_abc123"
```

Response `200`:

```json
{ "deleted": true, "file_id": "x7k2p9" }
```

---

## Agent signup

`POST /v1/agent/signup`

Agent self-provisioning. No human required, no dashboard visit. Rate-limited to 5 signups/hour per IP.

Request: `application/json`

```json
{
  "email": "agent@workflow.ai",
  "agent_name": "my-n8n-workflow"
}
```

Response `200`:

```json
{
  "api_key": "curly_live_abc123",
  "plan": "free",
  "uploads_remaining": 100,
  "message": "API key created. Store it securely — it won't be shown again."
}
```

The key is shown exactly once. Store it.

---

## Usage

`GET /v1/usage`

Usage stats for the authenticated key. Poll this to decide whether to throttle, upgrade, or fail over.

```bash
curl https://curlyflies.com/v1/usage \
  -H "Authorization: Bearer $CURLY_KEY"
```

Response `200`:

```json
{
  "plan": "builder",
  "uploads_this_month": 847,
  "uploads_limit": 2000,
  "uploads_remaining": 1153,
  "reset_date": "2026-07-01T00:00:00Z"
}
```

---

## Plans, limits, and rate limits

| Plan | Price | Uploads/mo | Max size | TTL options | Rate limit |
|---|---|---|---|---|---|
| Free | $0 | 100 | 5MB | 24h only | 100 req/hour |
| Builder | $9/mo | 2,000 | 250MB | up to 30 days | 500 req/hour |
| Pro | $29/mo | 10,000 | 1GB | up to 60 days | 2,000 req/hour |
| Scale | $99/mo | 50,000 | 5GB | up to 90 days | 10,000 req/hour |

Overage: $0.002 per upload above the plan limit. When rate-limited, responses are `429` with a `Retry-After` header.

## Allowed file types

`image/jpeg`, `image/png`, `image/gif`, `image/webp`, `video/mp4`, `video/quicktime`, `application/pdf`, `text/plain`, `application/json`.

Rejected: executables and standalone `.exe`, `.sh`, `.php`, `.py`, `.js`, `.html` uploads. Magic bytes are validated against the declared content type for every upload.

## Error code reference

| Code | Meaning |
|---|---|
| `401` | Invalid or missing API key |
| `404` | File does not exist or has expired |
| `413` | File exceeds plan size limit |
| `415` | Disallowed file type or magic-byte mismatch |
| `429` | Rate limit exceeded (`Retry-After` header included) |
| `9004` | (Instagram-side) File URL not publicly accessible — use a curlyflies URL instead |

## OpenAPI

A machine-readable spec is served at `https://curlyflies.com/v1/openapi.json`. Agent-readable docs live at [`/llms.txt`](https://curlyflies.com/llms.txt).
