# Image Upload API

This is an **optional** upload service provided by Social Print Studio to make integration easier. It gives you a simple way to upload images and receive publicly accessible URLs — which is exactly what the [Order Handoff API](/docs/order-handoff) requires when submitting orders.

The returned `publicUrl` can be passed directly into the `projectData.photos` array when calling the Order Handoff API.

> **When to use this:** If your application doesn't have its own backend or image hosting, or if you'd rather not manage file storage yourself, use this endpoint to upload customer photos directly to our S3 bucket.

> **When to skip this:** If your app already hosts images at publicly accessible URLs (e.g. via your own S3 bucket, Cloudinary, Firebase Storage, etc.), just pass those URLs directly to the Order Handoff API. You don't need this service.

## Endpoint

```
POST https://printkit.dev/api/upload
Authorization: Bearer <your-api-key>  (optional)
```

Upload files to S3 via presigned URLs. Supports images and other file types (PDFs, documents, etc.). Max file size is 50 MB per file. Optionally include an [API key](/docs/authentication) for attribution. If an invalid key is provided, the API returns `401`.

## Usage

The upload process is two steps: get a presigned upload URL, then send the file directly to S3 using that URL.

### Step 1: Get Upload URL

```javascript
const response = await fetch(
  'https://printkit.dev/api/upload',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`,  // optional — enables attribution
    },
    body: JSON.stringify({
      filename: 'photo.jpg',        // required
      source: 'my-app',             // required
      email: 'you@example.com',     // highly recommended
      fileSize: 1234567,            // optional - enables 50MB validation
      contentType: 'image/jpeg',    // optional - auto-detected from filename
      folder: 'my-app'              // optional - defaults to 'lambda-uploads'
    })
  }
);

const { uploadUrl, publicUrl } = await response.json();
```

```bash
curl -X POST https://printkit.dev/api/upload \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "filename": "photo.jpg",
    "source": "my-app",
    "email": "you@example.com",
    "fileSize": 1234567,
    "contentType": "image/jpeg",
    "folder": "my-app"
  }'

# Response:
# { "uploadUrl": "https://s3.amazonaws.com/...", "publicUrl": "https://..." }
```

```python
import httpx

response = httpx.post(
    "https://printkit.dev/api/upload",
    headers={
        "Authorization": f"Bearer {API_KEY}",  # optional — enables attribution
    },
    json={
        "filename": "photo.jpg",
        "source": "my-app",                      # required, your app name
        "email": "you@example.com",              # highly recommended
        "fileSize": 1234567,                     # optional - enables 50MB validation
        "contentType": "image/jpeg",             # optional - auto-detected from filename
        "folder": "my-app-name",                 # optional - defaults to 'lambda-uploads'
    },
)

data = response.json()
upload_url = data["uploadUrl"]
public_url = data["publicUrl"]
```

### Step 2: Upload File to S3

```javascript
await fetch(uploadUrl, {
  method: 'PUT',
  headers: {
    'Content-Type': 'image/jpeg'
  },
  body: file  // raw File object from a file input
});

// File is now live at publicUrl
```

```bash
curl -X PUT "UPLOAD_URL_FROM_STEP_1" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg

# File is now live at the publicUrl from Step 1
```

```python
import httpx

with open("photo.jpg", "rb") as f:
    upload_response = httpx.put(
        upload_url,  # from Step 1
        headers={"Content-Type": "image/jpeg"},
        content=f.read(),
    )

# File is now live at public_url from Step 1
```

> **Note:** The `x-amz-acl` parameter is baked into the presigned URL — do **not** send it as a header, or the request will fail.

## Complete Helper Function

A drop-in helper that handles both steps and returns the final public URL:

```javascript
async function uploadToS3(file, { apiKey, source, email, folder = 'my-app-name' }) {
  const headers = { 'Content-Type': 'application/json' };
  if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;

  const response = await fetch(
    'https://printkit.dev/api/upload',
    {
      method: 'POST',
      headers,
      body: JSON.stringify({
        filename: file.name,
        source,
        email,
        fileSize: file.size,
        contentType: file.type,
        folder
      })
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error || 'Failed to get upload URL');
  }

  const { uploadUrl, publicUrl } = await response.json();

  const uploadResponse = await fetch(uploadUrl, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file
  });

  if (!uploadResponse.ok) {
    throw new Error(`S3 upload failed: ${uploadResponse.status}`);
  }

  return publicUrl;
}

// Usage
const publicUrl = await uploadToS3(fileFromInput, {
  apiKey: API_KEY,
  source: 'my-app',
  email: 'you@example.com',
});

// Then use it in your order submission:
const orderResponse = await fetch(
  'https://printkit.dev/api/add-to-cart',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`,  // optional — enables attribution
    },
    body: JSON.stringify({
      sku: 'metal-print-4x4',
      source: 'my-app',
      email: 'you@example.com',
      projectData: {
        photos: [publicUrl],
      },
    }),
  }
);

const { redirectUrl } = await orderResponse.json();
window.location.href = redirectUrl;
```

```bash
# Step 1: Get a presigned upload URL
UPLOAD_RESPONSE=$(curl -s -X POST https://printkit.dev/api/upload \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "filename": "photo.jpg",
    "source": "my-app",
    "email": "you@example.com",
    "fileSize": 1234567,
    "contentType": "image/jpeg"
  }')

# Extract URLs from response (requires jq)
UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.uploadUrl')
PUBLIC_URL=$(echo $UPLOAD_RESPONSE | jq -r '.publicUrl')

# Step 2: Upload the file directly to S3
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary @photo.jpg

# Step 3: Use the public URL in your order submission
curl -X POST https://printkit.dev/api/add-to-cart \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d "{
    \"sku\": \"metal-print-4x4\",
    \"source\": \"my-app\",
    \"email\": \"you@example.com\",
    \"projectData\": {
      \"photos\": [\"$PUBLIC_URL\"]
    }
  }"
```

```python
import httpx
from pathlib import Path


def upload_to_s3(
    filepath: str,
    source: str,
    email: str | None = None,
    api_key: str | None = None,
    folder: str = "my-app-name",
) -> str:
    """Upload a file and return its public URL."""
    path = Path(filepath)
    headers = {}
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    # Step 1: Get presigned upload URL
    payload = {
        "filename": path.name,
        "source": source,
        "fileSize": path.stat().st_size,
        "contentType": "image/jpeg",
        "folder": folder,
    }
    if email:
        payload["email"] = email

    response = httpx.post(
        "https://printkit.dev/api/upload",
        headers=headers,
        json=payload,
    )
    response.raise_for_status()
    data = response.json()

    # Step 2: Upload file to S3
    upload_response = httpx.put(
        data["uploadUrl"],
        headers={"Content-Type": "image/jpeg"},
        content=path.read_bytes(),
    )
    upload_response.raise_for_status()

    return data["publicUrl"]


# Usage
public_url = upload_to_s3("photo.jpg", source="my-app", email="you@example.com", api_key=API_KEY)

# Then use it in your order submission:
order_response = httpx.post(
    "https://printkit.dev/api/add-to-cart",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "sku": "metal-print-4x4",
        "source": "my-app",
        "email": "you@example.com",
        "projectData": {
            "photos": [public_url],
        },
    },
)

redirect_url = order_response.json()["redirectUrl"]
```

## Request Parameters

### Required

| Parameter | Type | Description |
| --- | --- | --- |
| `filename` | string | The original filename. Used for the S3 key and content type detection. |
| `source` | string | An identifier for your application (e.g. `"my-app"`). Used for tracking and debugging. |

### Optional

| Parameter | Type | Description |
| --- | --- | --- |
| `email` | string | A contact email address. Highly recommended — allows us to reach out if there are any issues with your uploads. |
| `fileSize` | number | File size in bytes. Recommended — enables 50 MB validation before upload. |
| `contentType` | string | MIME type (e.g. `image/jpeg`, `application/pdf`). Auto-detected from filename if not provided. |
| `folder` | string | S3 subfolder path. Defaults to `lambda-uploads`. |

## Accepted Photo Formats

The Order Handoff API accepts **JPG** and **PNG** only. Photo URLs must end in `.jpg`, `.jpeg`, or `.png`.

| Format | Use case |
| --- | --- |
| JPG | Photos and images (preferred for most products) |
| PNG | Images with transparency, graphics, or lossless quality requirements |

## Notes

- **File size limit: 50 MB** per file (only enforced if `fileSize` is provided in the request)
- **Upload URL expires in 15 minutes** — complete the S3 upload promptly after receiving it
- Files are stored at: `s3://print-kit/lambda-uploads/{folder}/{timestamp}-{random}-{filename}`
- Public URL format: `https://print-kit.s3.us-east-1.amazonaws.com/...`
- Non-POST requests return `405 Method Not Allowed`

> **Next step:** Once you have publicly accessible image URLs from this service (or your own hosting), you're ready to submit an order. See the [Order Handoff API](/docs/order-handoff).
