<!-- Source: https://docs.geopera.com/api-reference/guides/uploads · Markdown for LLMs -->

# Uploading your own data

Geopera isn't only a place to buy imagery — you can bring your own rasters (COGs and
other geospatial files) into a project, where they become first-class STAC **items +
assets**: searchable, renderable, clippable, and analysable like any other data.

The upload is a session with four operations. You reserve quota, get a signed URL, push
bytes straight to storage, then finalize — which ingests the file into items and assets
and runs the post-upload pipeline.

| Step | Operation | Side-effect | Scope |
|---|---|---|---|
| Reserve a session | `uploads.initiate` | compute | `uploads:write` |
| Get a signed URL | `uploads.signed_url` | compute | `uploads:write` |
| (optional) report progress | `uploads.progress` | compute | `uploads:write` |
| Finalize → items + assets | `uploads.complete` | compute | `uploads:write` |
| Abandon | `uploads.fail` | compute | `uploads:write` |

> **Best practice — make it a COG.** Render and analytics are fastest when the uploaded
> raster is a Cloud-Optimized GeoTIFF with overviews and embedded statistics. See the
> COG conventions referenced in [Processing & analytics](/api-reference/guides/processing).

## 1. Initiate the session

`uploads.initiate` reserves storage quota up front (so a too-large upload fails fast
with `402` rather than half-landing) and requires editor/admin on the target project.

```bash
curl -s -X POST https://api.geopera.com/v1/op/uploads.initiate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "your-project-id",
    "file_count": 1,
    "total_bytes": 524288000,
    "transfer_method": "signed_url",
    "target_collection_id": "optional-collection-id"
  }'
```

```json
{ "id": "u_8f12..." }
```

Inputs: `project_id` (required), `file_count`, `total_bytes` (used for the quota
reservation), and optionally `target_collection_id` / `target_item_id` to ingest into
an existing collection or item, plus `asset_key` and `is_categorical` hints. The
returned `id` is the upload session id used by every later step.

If the org doesn't have enough storage quota, this returns `402 Payment Required` —
upgrade or free space before retrying.

## 2. Get a signed URL

`uploads.signed_url` mints a short-lived, presigned URL for one file. You upload the
bytes directly to object storage — they never pass through the API.

```bash
curl -s -X POST https://api.geopera.com/v1/op/uploads.signed_url \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "upload_id": "u_8f12...",
    "file_name": "harbour_2024.tif",
    "content_type": "image/tiff"
  }'
```

```json
{
  "upload_url": "https://storage.googleapis.com/...&X-Goog-Signature=...",
  "object_path": "uploads/u_8f12.../harbour_2024.tif"
}
```

## 3. Upload the bytes

PUT the file straight to `upload_url`:

```bash
curl -X PUT \
  -H "Content-Type: image/tiff" \
  --upload-file ./harbour_2024.tif \
  "<upload_url>"
```

For multi-file uploads, request one signed URL per file (repeat step 2) and PUT each.
Optionally report progress with `uploads.progress` (`upload_id`, `status`,
`bytes_uploaded`) — useful to drive a progress bar or for resumable clients.

## 4. Complete the upload

`uploads.complete` finalizes the session: it creates the STAC item(s) and asset
record(s) for the uploaded file(s) and runs the post-upload pipeline (which derives
band names, statistics, and a default visualization profile so the data is immediately
renderable).

```bash
curl -s -X POST https://api.geopera.com/v1/op/uploads.complete \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "upload_id": "u_8f12..." }'
```

```json
{ "id": "u_8f12...", "item_ids": ["it_3a9c..."] }
```

`complete` is a **producer**: each item it creates is recorded with a provenance edge
back to the upload session, in the same transaction (see
[Provenance & lineage](/api-reference/guides/provenance)). A completion that produces no
new item (already complete, metadata-only) is a legitimate no-op.

The new items now show up in `items.search` and can be rendered, clipped, and fed to
analytics like any other item.

## 5. Abandon a session

If something goes wrong client-side, release the reservation with `uploads.fail`:

```bash
curl -s -X POST https://api.geopera.com/v1/op/uploads.fail \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "upload_id": "u_8f12...",
    "error_message": "client aborted",
    "error_step": "transfer"
  }'
```

This marks the session failed and returns the reserved storage to your quota.
