# Map Generation

This document describes how to submit a map generation job, monitor its progress, and cancel jobs using raw HTTP requests against the Supabase backend.

All requests go through Supabase and require the project URL and anon key, referred to as `SUPABASE_URL` and `SUPABASE_ANON_KEY` throughout this document. Quota enforcement is handled server-side — the submit endpoint rejects requests that exceed the caller's annual km² allowance.

{% stepper %}
{% step %}

#### Authenticate

**`POST /auth/v1/token?grant_type=password`**

Exchange an email and password for a session containing an access token.

**Headers:**

| Header         | Value               |
| -------------- | ------------------- |
| `apikey`       | `SUPABASE_ANON_KEY` |
| `Content-Type` | `application/json`  |

**Body:**

```json
{
  "email": "pilot@example.com",
  "password": "hunter2"
}
```

**Example:**

```
POST <SUPABASE_URL>/auth/v1/token?grant_type=password
apikey: <SUPABASE_ANON_KEY>
Content-Type: application/json

{
  "email": "pilot@example.com",
  "password": "hunter2"
}
```

**Response 200:**

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 3600,
  "expires_at": 1711324800,
  "refresh_token": "v1.MjQ3...",
  "user": {
    "id": "d0d8c19e-1b2a-4c3d-8e4f-5a6b7c8d9e0f",
    "email": "pilot@example.com",
    "role": "authenticated"
  }
}
```

Save the `access_token` — every subsequent request uses it as a Bearer token.
{% endstep %}

{% step %}

#### Submit Map Generation Job

{% hint style="warning" %}
There is a hard per-map size cap of 100,000 km² regardless of map quota usage. If you require a larger map, contact <support@theseus.us>.
{% endhint %}

**`POST /functions/v1/submit-map-job`**

Submit a map generation job. The server validates the caller's quota before accepting the job. If the requested area would exceed the annual km² allowance, the request is rejected with a `403` and a descriptive error. On success, the server records the area and quota year automatically — no separate quota recording step is needed.

**Headers:**

| Header          | Value                   |
| --------------- | ----------------------- |
| `apikey`        | `SUPABASE_ANON_KEY`     |
| `Authorization` | `Bearer <access_token>` |
| `Content-Type`  | `application/json`      |

**Body:**

| Field       | Type      | Required | Description                                         |
| ----------- | --------- | -------- | --------------------------------------------------- |
| `area_name` | `string`  | Yes      | 3–50 chars: lowercase letters, numbers, underscores |
| `geometry`  | `Polygon` | Yes      | GeoJSON polygon of the map area                     |
| `org_id`    | `string`  | No       | Organization ID if generating under an org's quota  |

**Example:**

```
POST <SUPABASE_URL>/functions/v1/submit-map-job
apikey: <SUPABASE_ANON_KEY>
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "area_name": "downtown_austin",
  "geometry": {
    "type": "Polygon",
    "coordinates": [[
      [-97.75, 30.26],
      [-97.73, 30.26],
      [-97.73, 30.28],
      [-97.75, 30.28],
      [-97.75, 30.26]
    ]]
  }
}
```

**Response 200 (job accepted):**

```json
{
  "workflow_id": "wf-abc-123",
  "request_id": 42,
  "status": "queued",
  "message": "Map generation job submitted successfully",
  "area_name": "downtown_austin",
  "area_km2": 1.25,
  "quota_info": {
    "total_quota_km2": 500,
    "used_km2": 120.5,
    "remaining_km2": 378.25,
    "requested_km2": 1.25,
    "after_request_km2": 121.75
  }
}
```

Save the `workflow_id` — it is used for status polling, cancellation, and download.

**Response 403 (quota exceeded):**

```json
{
  "error": "Quota exceeded. You have 0.2 km² remaining, but requested 1.3 km².",
  "quota_info": {
    "total_quota_km2": 500,
    "used_km2": 499.8,
    "remaining_km2": 0.2,
    "requested_km2": 1.3,
    "after_request_km2": 501.1
  }
}
```

**Response 403 (no license):**

```json
{
  "error": "No active license found. Please purchase a license at https://dashboard.theseus.us",
  "quota_info": null
}
```

**Key fields:**

| Field                        | Description                                      |
| ---------------------------- | ------------------------------------------------ |
| `workflow_id`                | Unique identifier for this generation job        |
| `area_km2`                   | Calculated area of the submitted polygon in km²  |
| `quota_info.remaining_km2`   | Remaining quota after this submission            |
| `quota_info.total_quota_km2` | `0` means unlimited — no quota limit is enforced |
| {% endstep %}                |                                                  |

{% step %}

#### Poll Job Status

**`POST /functions/v1/map-workflow-status`**

Poll for real-time progress on a generation job. Returns the current state and percent complete.

**Headers:**

| Header          | Value                   |
| --------------- | ----------------------- |
| `apikey`        | `SUPABASE_ANON_KEY`     |
| `Authorization` | `Bearer <access_token>` |
| `Content-Type`  | `application/json`      |

**Body:**

| Field         | Type     | Required | Description                            |
| ------------- | -------- | -------- | -------------------------------------- |
| `workflow_id` | `string` | Yes      | The `workflow_id` from the submit step |

**Example:**

```
POST <SUPABASE_URL>/functions/v1/map-workflow-status
apikey: <SUPABASE_ANON_KEY>
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "workflow_id": "wf-abc-123"
}
```

**Response 200 (in progress):**

```json
{
  "state": "processing",
  "status": "Generating tiles (batch 3/8)",
  "percent_complete": 42,
  "area_name": "downtown_austin",
  "task_id": "wf-abc-123"
}
```

**Response 200 (completed):**

```json
{
  "state": "completed",
  "status": "Map generation complete",
  "percent_complete": 100,
  "area_name": "downtown_austin",
  "task_id": "wf-abc-123"
}
```

**Response 200 (failed):**

```json
{
  "state": "failed",
  "error": "Satellite imagery unavailable for the requested area",
  "area_name": "downtown_austin",
  "task_id": "wf-abc-123"
}
```

**Key `state` values:**

| State        | Meaning                                       |
| ------------ | --------------------------------------------- |
| `queued`     | Job accepted, waiting for a worker            |
| `started`    | Worker picked up the job                      |
| `processing` | Actively generating tiles                     |
| `completed`  | Generation finished, download available       |
| `failed`     | Generation failed — check `error` for details |
| `cancelled`  | Job was cancelled by the user                 |

**Recommended polling interval:** 5 seconds. Continue polling until `state` is `completed`, `failed`, or `cancelled`.
{% endstep %}

{% step %}

#### Cancel a Job (Optional)

**`POST /functions/v1/cancel-map-workflow`**

Cancel a running or queued map generation job.

**Headers:**

| Header          | Value                   |
| --------------- | ----------------------- |
| `apikey`        | `SUPABASE_ANON_KEY`     |
| `Authorization` | `Bearer <access_token>` |
| `Content-Type`  | `application/json`      |

**Body:**

| Field         | Type     | Required | Description                 |
| ------------- | -------- | -------- | --------------------------- |
| `workflow_id` | `string` | Yes      | The `workflow_id` to cancel |

**Example:**

```
POST <SUPABASE_URL>/functions/v1/cancel-map-workflow
apikey: <SUPABASE_ANON_KEY>
Authorization: Bearer <access_token>
Content-Type: application/json

{
  "workflow_id": "wf-abc-123"
}
```

**Response 200:**

```json
{
  "workflow_id": "wf-abc-123",
  "status": "cancelled",
  "message": "Workflow cancelled successfully",
  "area_name": "downtown_austin"
}
```

{% endstep %}

{% step %}

#### Confirm Completion via Supabase

Once the status endpoint reports `completed`, the map result will also appear in the Supabase `map_requests` table with a populated `map_results` join. This is the same query described in [Map Download — List Maps](/micro-vps/autopilot-integration/developer-api/map-download.md#list-maps).

Poll the Supabase table or filter by `workflow_id` to confirm the result is available:

**Headers:**

| Header          | Value                               |
| --------------- | ----------------------------------- |
| `apikey`        | `SUPABASE_ANON_KEY`                 |
| `Authorization` | `Bearer <access_token>`             |
| `Accept`        | `application/vnd.pgrst.object+json` |

**Example:**

```
GET <SUPABASE_URL>/rest/v1/map_requests?select=*,map_results(*)&workflow_id=eq.wf-abc-123&deleted_at=is.null
apikey: <SUPABASE_ANON_KEY>
Authorization: Bearer <access_token>
Accept: application/vnd.pgrst.object+json
```

When `map_results` is no longer `null` and `map_results.download_url` is present, the map is ready to download. Follow the [Map Download](/micro-vps/autopilot-integration/developer-api/map-download.md) flow from there.
{% endstep %}
{% endstepper %}

## Complete Flow Summary

```mermaid
flowchart TD
    A["POST /auth/v1/token?grant_type=password
    Headers: apikey
    Body: { email, password }
    ← access_token"] --> B

    B["POST /functions/v1/submit-map-job
    Headers: apikey, Authorization
    Body: { area_name, geometry, org_id? }
    ← { workflow_id, quota_info }"] --> C

    C{"Response status?"}
    C -- "200 OK" --> D
    C -- "403 Forbidden" --> X["Quota exceeded or no license
    — check error and quota_info"]

    D["POST /functions/v1/map-workflow-status
    Headers: apikey, Authorization
    Body: { workflow_id }
    ← { state, percent_complete }"] --> E

    E{"state?"}
    E -- "completed" --> F
    E -- "processing / queued" --> W["Wait 5s"]
    W -.-> D
    E -- "failed" --> Y["Handle error"]
    E -- "cancelled" --> Z["Job cancelled"]

    F["GET /rest/v1/map_requests
    Headers: apikey, Authorization
    ?workflow_id=eq.workflow_id
    ← map_results.download_url"] --> G

    G["Download .tar.gz archive
    (see Map Download doc)"]
```

## Geometry Format

The `geometry` field must be a valid [GeoJSON Polygon](https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6). Coordinates are `[longitude, latitude]` pairs (GeoJSON order). The ring must be closed — the first and last coordinate must be identical.

```json
{
  "type": "Polygon",
  "coordinates": [[
    [-97.75, 30.26],
    [-97.73, 30.26],
    [-97.73, 30.28],
    [-97.75, 30.28],
    [-97.75, 30.26]
  ]]
}
```

Minimum 4 coordinates (3 unique vertices + closing point). The server calculates the polygon area in km² using a spherical trapezoidal approximation.

## Area Name Requirements

| Rule         | Constraint                              |
| ------------ | --------------------------------------- |
| Length       | 3–50 characters                         |
| Characters   | Lowercase letters, numbers, underscores |
| Uniqueness   | Must not match an existing map name     |
| Sanitization | Hyphens are converted to underscores    |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.theseus.us/micro-vps/autopilot-integration/developer-api/map-generation.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
