---
title: Client
source: https://infrared.city/docs/sdk/1.0/python/sdk/
---

# Client

Infrared SDK client.

Provides `InfraredClient`, the main entry point for the Infrared City API. Supports area-level tiled orchestration, webhook integration, and building/vegetation/ground-material queries.

## BASE_URL_ENV_NAME  `module-attribute`

```python
BASE_URL_ENV_NAME = 'INFRARED_BASE_URL'
```

Name of the environment variable that supplies the API base URL.

## API_KEY_ENV_NAME  `module-attribute`

```python
API_KEY_ENV_NAME = 'INFRARED_API_KEY'
```

Name of the environment variable that supplies the API key.

## DEFAULT_BASE_URL  `module-attribute`

```python
DEFAULT_BASE_URL = 'https://api.infrared.city/v2'
```

API base URL used when no valid `base_url` is given.

## InfraredClient

Main client for the Infrared City API.

Supports area-level orchestration via `run_area` / `run_area_and_wait`, and webhook management via `self.webhooks`.

Resources for agents and integrators: the SDK documentation at https://infrared.city/docs/sdk/, and the agent skills (Claude Code / Cursor / Codex / Copilot / Windsurf) and runnable cookbook notebooks in https://github.com/Infrared-city/infrared-skills.

A single INFO log line on first instantiation per process surfaces these links to debug sessions. Set `INFRARED_QUIET=1` to silence.

Identifying your application

Every outbound request carries two telemetry headers so Infrared can tell which client made a call. Downstream integrators — plugins, connectors, other SDKs — should identify themselves:

- `application` → `x-infrared-application`: the calling SURFACE (`"qgis"`, `"arcgis"`, `"sketchup"`, …). Defaults to `"sdk"`. Any value is accepted; the gateway owns the list of known surfaces and records unrecognised ones as `other`.
- `sdk_id` → `x-infrared-sdk`: the calling LIBRARY and ITS OWN version, e.g. `"infrared-qgis/1.1.2"`. This SDK's own token is appended rather than replaced, so the wire carries `"infrared-qgis/1.1.2 infrared-sdk/<version>"` — the host is identified without losing which SDK version ran.

::

```python
client = InfraredClient(
api_key=...,
application="qgis",
sdk_id=f"infrared-qgis/{plugin_version}",
)
```

Both resolve as **argument → environment variable → default**, with `INFRARED_APPLICATION` / `INFRARED_SDK` as the env fallbacks for hosts that build the client through glue code and cannot pass arguments. A malformed value (blank, or carrying a control character — the header-injection case) raises `ValueError` here rather than failing on the first API call. Neither value can reach any header other than the two above, so neither can shadow `x-api-key`.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `api_key` | `str` | API key. When omitted it is read from the `INFRARED_API_KEY` environment variable. | `None` |
| `logger` | `Logger` | Logger for client messages. Defaults to this module's logger. | `None` |
| `base_url` | `str` | Absolute `http(s)` URL of the API, for example `"https://api.infrared.city/v2"`. Resolved as argument, then the `INFRARED_BASE_URL` environment variable, then the default `"https://api.infrared.city/v2"`. A value that is not an absolute `http(s)` URL is ignored with a warning and the next source is used. A trailing `/` is removed. | `None` |
| `transport` | `(json, binary)` | Default request representation for runs started by this client. `None` (default) lets each run choose: binary, except for analyses that have no binary route, which use JSON. A retry (`retry_from`) keeps the transport of the schedule it resumes. A `transport` argument to a run method overrides this value. | `"json"` |
| `execution_config` | `[ExecutionConfig](../analyses/#infrared_sdk.analyses.ExecutionConfig)` | Execution mode. `ExecutionConfig.exec_async` (default) is the only mode. | `[exec_async](../analyses/#infrared_sdk.analyses.ExecutionConfig.exec_async)` |
| `application` | `str` | Calling surface reported in `x-infrared-application` (see above). Defaults to `"sdk"`. | `None` |
| `sdk_id` | `str` | Calling library and version reported in `x-infrared-sdk` (see above). | `None` |
| `jobs_service_client` | `[JobsServiceClient](../analyses/#infrared_sdk.analyses.JobsServiceClient)` | Job client to use instead of the one this client creates. A client you inject is not closed by `close`. | `None` |
| `analysis_service_client` | `[AnalysisServiceClient](../analyses/#infrared_sdk.analyses.AnalysisServiceClient)` | Analysis client to use instead of the default. | `None` |
| `weather_service_client` | `[WeatherServiceClient](../layers/#infrared_sdk.layers.WeatherServiceClient)` | Weather client to use instead of the default. | `None` |
| `vegetation_service_client` | `[VegetationServiceClient](../vegetation/#infrared_sdk.vegetation.VegetationServiceClient)` | Vegetation client to use instead of the default. | `None` |
| `ground_materials_service_client` | `[GroundMaterialsServiceClient](../ground_materials/#infrared_sdk.ground_materials.GroundMaterialsServiceClient)` | Ground-material client to use instead of the default. | `None` |
| `landuse_service_client` | `[LandUseServiceClient](../landuse/#infrared_sdk.landuse.LandUseServiceClient)` | Land-use client to use instead of the default. | `None` |
| `buildings_service_client` | `[BuildingsServiceClient](../buildings/#infrared_sdk.buildings.BuildingsServiceClient)` | Buildings client to use instead of the default. | `None` |
| `webhooks_service_client` | `[WebhooksServiceClient](../webhooks/#infrared_sdk.webhooks.WebhooksServiceClient)` | Webhooks client to use instead of the default. | `None` |

Attributes:

| Name | Type | Description |
| --- | --- | --- |
| `api_key` | `_SharedApiKey` | Holder of the API key. Copying or pickling the client drops the key; create a new client instead of reusing a copy. |
| `telemetry` | `Telemetry` | The resolved `application` and `sdk_id` header values. |
| `execution_config` | `[ExecutionConfig](../analyses/#infrared_sdk.analyses.ExecutionConfig)` | Execution mode. |
| `transport` | `str or None` | Default transport, or `None` when each run chooses. |
| `base_url` | `str` | The resolved API base URL, without a trailing `/`. |
| `jobs` | `[JobsServiceClient](../analyses/#infrared_sdk.analyses.JobsServiceClient)` | Job submission, status and result access. |
| `analyses` | `[AnalysisServiceClient](../analyses/#infrared_sdk.analyses.AnalysisServiceClient)` | Single-tile analysis execution. |
| `weather` | `[WeatherServiceClient](../layers/#infrared_sdk.layers.WeatherServiceClient)` | Weather catalog access. |
| `vegetation` | `[VegetationServiceClient](../vegetation/#infrared_sdk.vegetation.VegetationServiceClient)` | Vegetation (tree) data access. |
| `ground_materials` | `[GroundMaterialsServiceClient](../ground_materials/#infrared_sdk.ground_materials.GroundMaterialsServiceClient)` | Ground-material data access. |
| `landuse` | `[LandUseServiceClient](../landuse/#infrared_sdk.landuse.LandUseServiceClient)` | Land-use data access. |
| `buildings` | `[BuildingsServiceClient](../buildings/#infrared_sdk.buildings.BuildingsServiceClient)` | Building data access. |
| `webhooks` | `[WebhooksServiceClient](../webhooks/#infrared_sdk.webhooks.WebhooksServiceClient)` | Webhook management. |

Raises:

| Type | Description |
| --- | --- |
| `ValueError` | If no API key is given and `INFRARED_API_KEY` is not set, if `application` or `sdk_id` is malformed, or if `transport` is not `"json"` or `"binary"`. |

Notes

An injected service client (the `*_service_client` arguments) keeps whatever telemetry it was constructed with on its own session, the same contract as its `api_key`. The area paths (`run_area`, `merge_area_jobs`, `check_area_state`) build their own per-thread clients and use the client-level value resolved here, not the injected client's. So an injected `jobs_service_client` carrying a different `application` reports its own on `client.jobs.submit()` and this client's on an area run. Pass `application` / `sdk_id` here rather than pre-labelling an injected client if you want one value everywhere.

### close

```python
close() -> None
```

Close the resources this client owns (sessions it created itself).

Service clients passed in through the `*_service_client` arguments are left open.

### preview_area

```python
preview_area(
polygon: dict,
max_tiles_override: Optional[int] = None,
analysis_type: Optional[str] = None,
*,
payload: Optional[AnalysesUnion] = None,
buildings: Optional[Mapping[str, dict]] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_sensors_per_job: Optional[float] = None,
terrain_context_margin_m: Optional[float] = None,
) -> AreaPreview
```

Preview tiling for a polygon without running any analyses.

Examples:

```python
# Wrong for solar — uses wind grid (256 m step)
preview = client.preview_area(polygon)

# Right — solar-radiation grid (512 m step)
preview = client.preview_area(
polygon, analysis_type="solar-radiation"
)
```

Wire-format analysis names (kebab-case, matching the API): `"wind-speed"`, `"pedestrian-wind-comfort"`, `"solar-radiation"`, `"direct-sun-hours"`, `"daylight-availability"`, `"sky-view-factors"`, `"thermal-comfort-index"`, `"thermal-comfort-statistics"`.

Warnings

The default grid is wind (256 m step). Calling `preview_area(polygon)` with no `analysis_type` returns the wind-grid tile count for backwards compatibility. Solar / daylight / thermal-comfort analyses run on a 512 m grid (~4x fewer tiles per area), so the default preview over-counts tiles by ~4x and under-estimates cost for solar-family workflows. Always pass `analysis_type` when you know which analysis you will run, with the same value your `run_area` payload uses, so the estimate matches what the run actually submits (and charges). Omitting `analysis_type` emits a `UserWarning` at runtime.

A facade estimate needs `payload=` (and usually `buildings=`). Requests with `analysis_surfaces` set are transparently split into multiple separately billed sub-jobs when a tile's estimated synthesized sensor count exceeds the server cap. Without `payload=`, this preview has no geometry to split and reports one job per tile -- an UNDER-estimate for a facade run. Pass the real payload (and buildings) to get the real count. See the README's "Facade & Terrain Analysis" section. Note that a repeated facade run still bills in full even when nothing on the client changed.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `polygon` | `dict` | A GeoJSON Polygon object. | *required* |
| `max_tiles_override` | `int` | Override the default maximum number of non-empty tiles. | `None` |
| `analysis_type` | `str` | Wire-format analysis name (see warning above). Selects the tile grid: wind types use a 256 m step (50 % overlap); solar / daylight / thermal-comfort types use a 512 m step (no overlap, ~4x fewer tiles per area). `None` (default) **falls back to the wind grid** for legacy callers — explicitly pass the analysis you intend to run for an accurate preview. **When `payload` is given, `payload.analysis_type` decides the grid** (matching `run_area`, which takes no separate `analysis_type` at all) and this parameter is ignored. | `None` |
| `payload` | `[AnalysesUnion](../analyses/#infrared_sdk.analyses.AnalysesUnion)` | The SAME payload you would pass to `run_area`. Required for an accurate facade (`analysis_surfaces`) estimate — see the warning above. Its `analysis_type` selects the tile grid (see `analysis_type` above); otherwise ignored for pricing beyond selecting the grid and (with `analysis_surfaces` set) running the offline batch split; nothing here is submitted. | `None` |
| `buildings` | `mapping` | The same per-analysis layers `run_area` accepts. Only read when `payload` carries `analysis_surfaces` — a facade batch count depends on which buildings land in which tile. | `None` |
| `vegetation` | `mapping` | The same per-analysis layers `run_area` accepts. Only read when `payload` carries `analysis_surfaces` — a facade batch count depends on which buildings land in which tile. | `None` |
| `ground_materials` | `mapping` | The same per-analysis layers `run_area` accepts. Only read when `payload` carries `analysis_surfaces` — a facade batch count depends on which buildings land in which tile. | `None` |
| `max_sensors_per_job` | `float` | Per-job sensor cap for a facade payload, in retained sensors: a whole number from 1 to 250 000. A fractional value is rounded down with a `DeprecationWarning`. Passed to the same offline batch split `run_area` uses, so the estimate matches a real run built with the same cap. | `None` |
| `terrain_context_margin_m` | `float` | Terrain reach in metres, as for `run_area`. Only used for the facade batch split. | `None` |

Returns:

| Type | Description |
| --- | --- |
| `[AreaPreview](../tiling/#infrared_sdk.tiling.AreaPreview)` | `tile_count` (int): number of non-empty tiles on the grid for `analysis_type`. `would_bill_jobs` (int): the planned job count -- equal to `tile_count` for a grid analysis, and the real (higher) count of facade sub-batches when `payload` carries `analysis_surfaces`. **Price from this field, not `tile_count`.** `sensor_count` (int or None): total synthesized sensors across the planned facade batches, or `None` off a grid preview. `estimated_time_s` / `estimated_cost_tokens`: scaled by `would_bill_jobs` for **one** analysis at that grid. Multi-analysis workflows must multiply by the number of analyses on the same grid family. |

Raises:

| Type | Description |
| --- | --- |
| `PolygonValidationError` | If `polygon` is not a valid GeoJSON polygon, if `max_tiles_override` is not a non-negative integer, or if the polygon produces more tiles than the limit. It is a subclass of `ValueError`. |
| `ValueError` | If `max_sensors_per_job` or `terrain_context_margin_m` is not a valid number, or if a supplied layer is invalid. |

Warns:

| Type | Description |
| --- | --- |
| `UserWarning` | If neither `analysis_type` nor `payload` is given, because the preview then uses the wind grid. |

### merge_buildings  `staticmethod`

```python
merge_buildings(buildings_map: Dict[str, Optional[dict]]) -> dict
```

Combine per-tile buildings dicts into one for visualization.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `buildings_map` | `dict[str, dict or None]` | Maps tile id to that tile's buildings dict, or `None`. | *required* |

Returns:

| Type | Description |
| --- | --- |
| `dict` | The combined buildings dict: list values are concatenated and other keys are taken from the last dict that has them. An empty dict if every value is `None`. |

### run_area

```python
run_area(
payload: AnalysesUnion,
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
retry_from: Optional[AreaSchedule] = None,
known: Optional[Dict[str, Any]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
) -> AreaSchedule
```

```python
run_area(
payload: List[AnalysesUnion],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
retry_from: Optional[AreaSchedule] = None,
known: Optional[Dict[str, Any]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
) -> List[AreaSchedule]
```

```python
run_area(
payload: Union[AnalysesUnion, List[AnalysesUnion]],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
retry_from: Optional[AreaSchedule] = None,
known: Optional[Dict[str, Any]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
) -> Union[AreaSchedule, List[AreaSchedule]]
```

Submit tiled analysis jobs over a polygon.

`max_workers` sizes the submit pool: how many tile POSTs are in flight at once. The default is `DEFAULT_SUBMIT_WORKERS` (8), where throughput measurably stops improving; a caller who has measured their own workload may ask for up to `MAX_SUBMIT_WORKERS` (20). It is NOT the merge pool — `merge_area_jobs` sizes that one separately — and it does nothing for the first few seconds of a run, which are serial layer preparation.

Layer parameters (buildings, vegetation, ground_materials): `None` or `{}` skips injection (empty per tile), a non-empty mapping uses the provided data. Do not mutate the layer mappings while a call runs. `buildings` also accepts the `AreaBuildings` object `buildings.get_area` returns, and then re-anchors from the frame that object RECORDS — so acquiring once for a large polygon and running several sub-areas places the buildings correctly. A bare map carries no frame and is read as being in the run polygon's own, which is why the README says to acquire with the polygon you run.

`buildings` entries must carry both `coordinates` and `indices` — as `DotBimMesh` or as a dict; the server discards a mesh missing either, before compute and after the charge, so one is refused here. `vegetation` features may be `Point`, `Polygon` or `MultiPolygon`; a polygon is seated at its outer ring's centroid. Any other vegetation geometry type raises.

`retry_from` (resubmit only the failed tiles/batches of a prior run, carrying the succeeded jobs forward) resubmits `failed_submissions` only; `uncertain_submissions` (a tile whose request may already have created a job) are carried forward untouched and never resubmitted. `retry_from` describes exactly one payload's prior run, so it is **not supported with a multi-payload list**: `run_area([A, B], polygon, retry_from=schedule_B)` raises `ValueError`. Retry each payload separately — `run_area(B, polygon, retry_from=schedule_B)`. The retry payload must also match the original run's `config_hash` and buildings map (both guarded — a mismatch raises).

`terrain_context_margin_m` widens how far `ground_geometry` is sliced beyond each tile. The default (`None`) covers the buildings and trees the tile actually analyses and nothing more. This extent is independent of `terrain_alignment`: the SDK's supplied-scene default is `as-is` and does not seat or validate those solids. Long-range relief belongs in `context_geometry`. Raise it only for a site where distant terrain genuinely shades the tile (a valley, an escarpment); the payload grows roughly with the square of the reach. Values below the tile config's own context margin are floored to it, so this can never strand a building or tree that was admitted to the tile on absent ground.

An interrupt (`KeyboardInterrupt`, `SystemExit`, a timeout that cancels the calling thread) part-way through submission still propagates -- this method never swallows it, and never cancels a tile already accepted server-side. It DOES stop a tile the submit pool had not yet reached: nothing new goes out over the network once the interrupt lands. A tile whose request was already in flight when the interrupt landed still runs to completion and is recorded normally -- only work that had not started is cancelled. What the interrupt adds: the exception carries a `partial_schedule` attribute -- an `AreaSchedule` (a list of them for a multi-payload call), built from every tile that had an outcome before the interrupt landed. Pass it straight to `run_area(..., retry_from=exc.partial_schedule)` to resubmit only what is still missing; the tiles already accepted are carried forward, not re-billed. `partial_schedule` is only set when at least one tile had reached the submit pool; an interrupt during local planning (tiling, site preparation), before any request was sent, leaves nothing to resume and nothing is attached.

`on_accepted(job_id, tile_key)`, when given, is called once for each job the submit loop records, at the moment it records it, so a caller can store accepted job ids before this method returns (a process that is killed then does not lose them). `tile_key` is the schedule key (`tile_id` or `{tile_id}#batch{i}`); for a multi-payload list the same key can occur once per payload. It is not called for a failed or uncertain submission, nor for jobs that `retry_from` carries forward. It is a read-only observer: it is called from the submit worker threads, one call at a time, and an exception from it is logged and ignored, so it never changes the schedule or what is billed.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `payload` | `AnalysesUnion or list of AnalysesUnion` | The analysis to run, or a list of analyses to run over the same polygon. Only area analyses are accepted (not the interior models), and `sensor_points` payloads are not supported. In a list, no two payloads may be identical. | *required* |
| `polygon` | `dict` | A GeoJSON Polygon object that defines the area. | *required* |
| `buildings` | `[AreaBuildings](../buildings/#infrared_sdk.buildings.AreaBuildings) or mapping` | Building meshes, as the `AreaBuildings` object returned by `buildings.get_area` or a mapping of building id to `DotBimMesh` or dict. See above. | `None` |
| `vegetation` | `mapping` | Vegetation features, keyed by id. See above. | `None` |
| `ground_materials` | `mapping` | Ground-material layers: a mapping of material name to a GeoJSON FeatureCollection, for example the `layers` of the area object that `ground_materials` returns. An unknown material name raises `ValueError`. | `None` |
| `max_tiles_override` | `int` | Override the default maximum number of non-empty tiles. | `None` |
| `max_workers` | `int` | Size of the submit pool; see above. Defaults to 8. | `DEFAULT_SUBMIT_WORKERS` |
| `webhook_url` | `str` | URL to notify about the submitted jobs. On a retry, the value saved in `retry_from` is used when this is `None`. | `None` |
| `webhook_events` | `list of str` | Webhook events to subscribe to. On a retry, the saved value is used when this is `None`. | `None` |
| `retry_from` | `[AreaSchedule](../tiling/#infrared_sdk.tiling.AreaSchedule)` | A schedule from a previous call, to resubmit only its failed tiles. See above. | `None` |
| `known` | `dict` | A job id to status map from a previous `check_area_state` call. Only read together with `retry_from`, to avoid extra status requests. | `None` |
| `max_sensors_per_job` | `int` | Per-job sensor cap for facade (`analysis_surfaces`) payloads, in retained sensors: a whole number from 1 to 250 000. A fractional value is rounded down with a `DeprecationWarning`. Ignored, with a `UserWarning`, for payloads without `analysis_surfaces`. On a retry it must match the saved schedule. | `None` |
| `terrain_context_margin_m` | `float` | How far, in metres, ground geometry is sliced beyond each tile. See above. | `None` |
| `transport` | `(json, binary)` | Request representation. `None` (default) uses the saved schedule's transport on a retry, otherwise the client's `transport`, otherwise binary (JSON for analyses that have no binary route). | `"json"` |
| `on_accepted` | `callable` | Called as `on_accepted(job_id, tile_key)` for each accepted job. See above. | `None` |

Returns:

| Type | Description |
| --- | --- |
| `AreaSchedule or list of AreaSchedule` | The record of the submitted jobs, to pass to `check_area_state`, `merge_area_jobs` or `retry_from`. A list of schedules, one per payload and in the same order, when `payload` is a list. A tile whose submission failed is recorded on the schedule (`failed_submissions`, or `uncertain_submissions` when the outcome is unknown) rather than raised. |

Raises:

| Type | Description |
| --- | --- |
| `PolygonValidationError` | If `polygon` is not a valid GeoJSON polygon or produces more tiles than the limit. It is a subclass of `ValueError`. |
| `ValueError` | If a payload is not an area analysis or is a `sensor_points` payload, if payloads in a list are identical, if `transport`, `max_sensors_per_job` or `terrain_context_margin_m` is invalid, if a layer is invalid, or if `retry_from` is used with a list of more than one payload or does not match this call (a different payload, weather, site inputs, terrain margin, sensor cap or transport, or a schedule written by an older SDK version that cannot be resumed). |
| `KeyboardInterrupt` | If the call is interrupted; see above for `partial_schedule`. |

### check_area_state

```python
check_area_state(
schedule: AreaSchedule, *, known: Optional[Dict[str, Any]] = None
) -> AreaState
```

Query job statuses and return the area state.

`known` is the answer from a PREVIOUS call: a job id -> status map, updated IN PLACE. A job whose recorded status is terminal (`succeeded` or `failed`) is not asked about again — pass the same dict across repeated calls to skip re-querying every finished job. Also read by a `run_area(retry_from=...)` call that passes its own `known`, so a retry plan built from a prior poll issues no extra status requests.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `schedule` | `[AreaSchedule](../tiling/#infrared_sdk.tiling.AreaSchedule)` | The schedule returned by `run_area`. | *required* |
| `known` | `dict` | A job id to status map from a previous call, updated in place with the statuses found by this call. | `None` |

Returns:

| Type | Description |
| --- | --- |
| `[AreaState](../tiling/#infrared_sdk.tiling.AreaState)` | A snapshot of the job statuses: per-job states, counts per status, and whether the area is complete. A job whose status could not be read is reported as `unknown` and is asked about again on the next call. |

### merge_area_jobs

```python
merge_area_jobs(
schedule: AreaSchedule,
max_workers: int = DEFAULT_MERGE_WORKERS,
*,
strategy: str = "default",
wind_direction_deg: Optional[float] = None,
_known_states: Optional[Dict[str, Any]] = None,
) -> Union[AreaResult, SurfaceAnalysisResult]
```

Download and merge results for succeeded jobs in a schedule.

Each tile is merged as soon as its download completes; the SDK keeps no copy of it. `merged_grid` keeps the dtype the server sent: `float16`, `float32`, `int16` (UTCI/TCI, with `value_divisor` and `valid`) or, for a run that mixes dtypes, `float64`. Use `AreaResult.physical_grid()` for physical values.

Can be called at any time; it does not require every job to be finished.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `schedule` | `[AreaSchedule](../tiling/#infrared_sdk.tiling.AreaSchedule)` | The schedule returned by `run_area`. | *required* |
| `max_workers` | `int` | Width of the DOWNLOAD pool, separate from the submit pool `run_area` uses. Defaults to `DEFAULT_MERGE_WORKERS` (8). The only measurement behind that number is that 1 is too slow; 4 and above measured the same. | `DEFAULT_MERGE_WORKERS` |
| `strategy` | `str` | `"default"` — plain centre-crop merge. `"directional_blend"` — directional blend. Wind-speed only; requires `wind_direction_deg` or raises `ValueError`. | `'default'` |
| `wind_direction_deg` | `float` | Meteorological wind-from direction in degrees (0=N, 90=E, 270=W). | `None` |

Returns:

| Type | Description |
| --- | --- |
| `[AreaResult](../tiling/#infrared_sdk.tiling.AreaResult) or SurfaceAnalysisResult` | An `AreaResult` with the merged grid for a grid analysis, or a `SurfaceAnalysisResult` for a facade (`analysis_surfaces`) or custom-sensor run. |

Raises:

| Type | Description |
| --- | --- |
| `AreaRunError` | If every job failed, or if any tile did not contribute a result, so that a grid with holes is never returned silently. |
| `ValueError` | If `strategy` is not `"default"` for an analysis other than wind-speed, or if `strategy` is `"directional_blend"` without `wind_direction_deg`. |

### forget_schedule

```python
forget_schedule(schedule: AreaSchedule) -> None
```

Release the captures this client kept for the jobs of `schedule`.

Call it for a schedule you will never merge. The client keeps the capture of every facade or roof job until the join consumes it, and a schedule you drop cannot tell the client it is done. Jobs over the same geometry share one stored copy, so this frees only the copy that no other job of this client still uses. After this call `merge_area_jobs(schedule)` still merges, but without the outline (`columns.render_buffers()` then raises `ValueError`).

Warnings

A schedule made with `retry_from=other` carries the same job ids as `other` for the jobs it did not resubmit. A job id holds one reference, so forgetting one of the two releases those jobs for both.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `schedule` | `[AreaSchedule](../tiling/#infrared_sdk.tiling.AreaSchedule)` | The schedule whose captures are released. | *required* |

### run_area_and_wait

```python
run_area_and_wait(
payload: AnalysesUnion,
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
job_timeout: int = 300,
area_timeout: int = 3600,
on_progress: Optional[Callable[[AreaState], None]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
retries: int = 1,
) -> Union[AreaResult, SurfaceAnalysisResult]
```

```python
run_area_and_wait(
payload: List[AnalysesUnion],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
job_timeout: int = 300,
area_timeout: int = 3600,
on_progress: Optional[Callable[[AreaState], None]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
retries: int = 1,
) -> List[Union[AreaResult, SurfaceAnalysisResult]]
```

```python
run_area_and_wait(
payload: Union[AnalysesUnion, List[AnalysesUnion]],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
job_timeout: int = 300,
area_timeout: int = 3600,
on_progress: Optional[Callable[[AreaState], None]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
retries: int = 1,
) -> Union[
AreaResult,
SurfaceAnalysisResult,
List[Union[AreaResult, SurfaceAnalysisResult]],
]
```

Submit area jobs, poll until complete, merge and return results.

`max_workers` reaches the SUBMIT pool only. The merge download pool keeps its own width: it always runs at `DEFAULT_MERGE_WORKERS` (8) and is not reachable from here. A caller who needs to size the merge pool runs the three steps themselves — `run_area`, `check_area_state` and your own polling loop, then `merge_area_jobs(schedule, max_workers=...)` — which is the same sequence this method performs. This method starts bounded download, decode and per-tile folding when polling confirms a successful job. It returns results only after the final states and merge checks pass.

`retries` (default 1; 0 turns it off): resubmit failed, compute-failed and uncertain keys with the terminal states already learned, up to `retries` rounds. An incomplete merge raises only after the last round. No retry after a 402 abort.

Parameters:

| Name | Type | Description | Default |
| --- | --- | --- | --- |
| `payload` | `AnalysesUnion or list of AnalysesUnion` | The analysis to run, or a list of analyses to run over the same polygon. See `run_area`. | *required* |
| `polygon` | `dict` | A GeoJSON Polygon object that defines the area. | *required* |
| `buildings` | `mapping` | Input layers, as for `run_area`. | `None` |
| `vegetation` | `mapping` | Input layers, as for `run_area`. | `None` |
| `ground_materials` | `mapping` | Input layers, as for `run_area`. | `None` |
| `job_timeout` | `int` | Accepted for compatibility; it has no effect on this method. | `300` |
| `area_timeout` | `int` | Seconds to wait for all jobs to finish in each round. Defaults to 3600. | `3600` |
| `on_progress` | `callable` | Called with an `AreaState` after each status check. | `None` |
| `max_sensors_per_job` | `int` | Per-job sensor cap for facade payloads; see `run_area`. | `None` |
| `terrain_context_margin_m` | `float` | Terrain reach in metres; see `run_area`. | `None` |
| `max_tiles_override` | `int` | Override the default maximum number of non-empty tiles. | `None` |
| `max_workers` | `int` | Size of the submit pool; see above. Defaults to 8. | `DEFAULT_SUBMIT_WORKERS` |
| `webhook_url` | `str` | URL to notify about the submitted jobs. | `None` |
| `webhook_events` | `list of str` | Webhook events to subscribe to. | `None` |
| `transport` | `(json, binary)` | Request representation; see `run_area`. Defaults to the client's `transport`. | `"json"` |
| `on_accepted` | `callable` | Called as `on_accepted(job_id, tile_key)` for each accepted job; see `run_area`. | `None` |
| `retries` | `int` | Number of retry rounds; see above. Defaults to 1. | `1` |

Returns:

| Type | Description |
| --- | --- |
| `[AreaResult](../tiling/#infrared_sdk.tiling.AreaResult) or SurfaceAnalysisResult or list` | The merged result: an `AreaResult` for a grid analysis, or a `SurfaceAnalysisResult` for a facade or custom-sensor run. A list with one result per payload, in the same order, when `payload` is a list. |

Raises:

| Type | Description |
| --- | --- |
| `[AreaTimeoutError](../tiling/#infrared_sdk.tiling.AreaTimeoutError)` | If `area_timeout` is reached before all jobs finish. |
| `AreaRunError` | If, after the last retry round, every job failed or any tile did not contribute a result. |
| `ValueError` | For the invalid inputs described under `run_area`. |
