# SDK

The Cecil SDK is an [open source](https://github.com/cecilearth/sdk) Python library that allows you to use the Cecil platform. The SDK integrates with Cecil's internal API using standard HTTP methods, authentication, and response codes. Data resources use RFC 3339 timestamps and universally unique identifiers (UUID v4).

# Installation

Install the SDK in your project [virtual environment](https://docs.python.org/3/tutorial/venv.html). Python ≥ 3.10 is required.

```bash
pip install cecil
```

# Authentication

Configure the SDK by setting the `CECIL_API_KEY` environment variable. Only store your API key in an encrypted vault or secrets manager, never in code or plain/text files. Don't have an account? [Sign up](https://docs.cecil.earth/getting-started).

```bash
# Linux and macOS
export CECIL_API_KEY="my-api-key"

# Windows PowerShell
$env:CECIL_API_KEY = "my-api-key"
```

# Usage

All SDK functions require authentication unless otherwise specified.

```python
import cecil

client = cecil.Client()

client.function_name()
```

# AOI

The area of interest (AOI) represents a geographic area.

| Property | Type | Description |
| --- | --- | --- |
| `id` | uuid | Unique identifier. |
| `geometry` required | object | GeoJSON geometry object in `EPSG:4326` delimiting the boundary. |
| `external_ref` | string | External reference in your system, e.g. AOI ID or name. |
| `hectares` | float | Total size derived from the geometry. This is useful for cost tracking. |
| `created_at` | datetime | Current system time when the AOI was created. |
| `created_by` | uuid | Authenticated user who created the AOI. |
| `archived_at` | datetime | Current system time when the AOI was archived, or null. |
| `archived_by` | uuid | Authenticated user who archived the AOI, or null. |

The AOI `geometry` is a GeoJSON geometry object in `EPSG:4326` delimiting the AOI boundaries.

| Property | Type | Description |
| --- | --- | --- |
| `type` required | string | Polygon or MultiPolygon. |
| `coordinates` required | array | Nested array of coordinates according to the geometry `type`. |

## Create AOI

This function allows you to create an AOI.

**Input**

```python
client.create_aoi(
    external_ref="123",
    geometry={
        "type": "Polygon",
        "coordinates": [[
            [132.52934211276073, -12.721072673008706],
            [132.52934211276073, -12.730063400794094],
            [132.54027735328083, -12.730063400794094],
            [132.54027735328083, -12.721072673008706],
            [132.52934211276073, -12.721072673008706],
        ]],
    },
)
```

**Output**

```plaintext
AOI(
    id="fa200894-4e13-456c-872a-ba8efcda0812",
    external_ref="123",
    geometry={
        "type": "Polygon",
        "coordinates": [[
            [132.52934211276073, -12.721072673008706],
            [132.52934211276073, -12.730063400794094],
            [132.54027735328083, -12.730063400794094],
            [132.54027735328083, -12.721072673008706],
            [132.52934211276073, -12.721072673008706],
        ]],
    },
    hectares=118.12168458669186,
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
    archived_at=None,
    archived_by=None,
)
```

## List AOIs

This function allows you to list all AOIs that are currently active (not archived) in your organisation. You can optionally pass in `archived=True` to list all archived AOIs.

**Input**

```python
client.list_aois(archived=False)
```

**Output**

```plaintext
[
    AOI(
        id="fa200894-4e13-456c-872a-ba8efcda0812",
        external_ref="123",
        hectares=118.12168458669186,
        created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
        created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
        archived_at=None,
        archived_by=None,
    ),
]
```

## Get AOI

This function allows you to get an AOI by ID, regardless of being archived.

**Input**

```python
client.get_aoi("fa200894-4e13-456c-872a-ba8efcda0812")
```

**Output**

```plaintext
AOI(
    id="fa200894-4e13-456c-872a-ba8efcda0812",
    external_ref="123",
    geometry={
        "type": "Polygon",
        "coordinates": [[
            [132.52934211276073, -12.721072673008706],
            [132.52934211276073, -12.730063400794094],
            [132.54027735328083, -12.730063400794094],
            [132.54027735328083, -12.721072673008706],
            [132.52934211276073, -12.721072673008706],
        ]],
    },
    hectares=118.12168458669186,
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
    archived_at=None,
    archived_by=None,
)
```

## Archive AOI

This function allows you to archive an AOI by ID. When you archive an AOI, associated subscriptions remain available, but new subscriptions can no longer be created for the archived AOI.

**Input**

```python
client.archive_aoi("fa200894-4e13-456c-872a-ba8efcda0812")
```

**Output**

```plaintext
No content
```

## Restore AOI

This function allows you to restore an AOI by ID. You can restore an AOI from the archive at any time.

**Input**

```python
client.restore_aoi("fa200894-4e13-456c-872a-ba8efcda0812")
```

**Output**

```plaintext
No content
```

# Dataset

The dataset represents a product offering by a data provider. All dataset information available in the SDK uses the same source of truth as the list of [datasets](https://docs.cecil.earth/datasets) in the documentation. This allows you to programmatically analyse datasets before making a data acquisition. All attributes are provided, unless otherwise specified as optional.

```plaintext
Dataset(
    id="f42f8f48-6cf9-48af-8453-be6cca46acb8",
    name="Dataset name",
    type="Raster|Vector",
    crs="EPSG:4326", # optional; when null, see description

    description=[
        "Multi paragraph description",
    ],

    usage_notes=[
        "Multi paragraph usage notes",
    ],

    categories=[
        "Dataset category",
    ],

    licence=Licence(
        type="Open|Commercial",
    ),

    version=Version(
        number="1.0",      # optional
        date="2026-01-01", # optional
    ),

    spatial_coverage=SpatialCoverage(
        nominal="Global", # optional
    ),

    spatial_resolution=SpatialResolution(
        nominal="30 m",   # optional
        units="degrees",  # optional
        x=0.0025,         # optional
        y=0.0025,         # optional
    ),

    temporal_coverage=TemporalCoverage(
        nominal="2015+", # optional
    ),

    temporal_resolution=TemporalResolution(
        nominal="Annual", # optional
    ),

    constraints=Constraints(
        aoi_min_hectares=1,   # optional
        aoi_max_hectares=10,  # optional
        aoi_min_latitude=1,   # optional
        aoi_max_latitude=10,  # optional
        aoi_max_vertices=100, # optional
        aoi_geometry_types=["Polygon", "MultiPolygon"],
        organisation_verified_only=False,
    ),

    variables=[
        Variable(
            name="variable_name",
            type="float32",
            no_data="255", # optional
            units="m³",    # optional
            description=[
                "Multi paragraph description",
            ],
            usage_notes=[
                "Multi paragraph usage notes", # optional
            ],
            reference_table=[
                {}, # optional
            ],
        ),
    ],

    pricing=Pricing(
        description=[
            "Multi paragraph description",
        ],
        tiers=[
            Tier( # optional; e.g. open licence, or see description
                volume=Volume(
                    nominal="Any",
                ),
                price=Price(
                    amount=0.10,
                    nominal="$0.10/ha",
                ),
            ),
        ],
    ),

    resources=[
        Resource(
            type="Licensing",
            title="Terms of Service",
            href="https://example.com/terms",
        ),
    ],

    providers=[
        Provider( # the main provider is the first one in the list
            name="Provider name",
            website="https://example.com",
            description=[
                "Multi paragraph description",
            ],
        ),
    ],
)
```

## List datasets

This function allows you to list all datasets available in the Cecil platform.

**Input**

```python
client.list_datasets()
```

**Output**

```plaintext
[
    Dataset(
        # Omitting large payload for readability
        # Please refer to the full specification above
    ),
]
```

## Get dataset

This function allows you to get a dataset by ID.

**Input**

```python
client.get_dataset("f42f8f48-6cf9-48af-8453-be6cca46acb8")
```

**Output**

```plaintext
Dataset(
    # Omitting large payload for readability
    # Please refer to the full specification above
),
```

# Subscription

The subscription represents a dataset acquisition for your AOI.

| Property | Type | Description |
| --- | --- | --- |
| `id` | uuid | Unique identifier. |
| `aoi_id` required | string | Unique identifier of the AOI associated with this subscription. |
| `dataset_id` required | string | Unique identifier of the dataset associated with this subscription. You can find this information on the details page of available [datasets](https://docs.cecil.earth/datasets). |
| `external_ref` | string | External reference in your system, e.g. data subscription ID, use case ID, or description of the use case. |
| `created_at` | datetime | Current system time when the subscription was created. |
| `created_by` | uuid | Authenticated user who created the subscription. |
| `archived_at` | datetime | Current system time when the subscription was archived, or null. |
| `archived_by` | uuid | Authenticated user who archived the subscription, or null. |

## Create subscription

This function allows you to acquire datasets for your AOI. This process runs in the background and may take from a few minutes to a few business days depending on the data provider.

**Input**

```python
client.create_subscription(
    aoi_id="fa200894-4e13-456c-872a-ba8efcda0812",
    dataset_id="f42f8f48-6cf9-48af-8453-be6cca46acb8",
    external_ref="123",
)
```

**Output**

```plaintext
Subscription(
    id="f644efb3-8ba4-4bc4-8333-1ace54a3f111",
    aoi_id="fa200894-4e13-456c-872a-ba8efcda0812",
    dataset_id="f42f8f48-6cf9-48af-8453-be6cca46acb8",
    external_ref="123",
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
    archived_at=None,
    archived_by=None,
)
```

## List subscriptions

This function allows you to list all subscriptions that are currently active (not archived) in your organisation. You can optionally pass in `archived=True` to list all archived subscriptions.

**Input**

```python
client.list_subscriptions(archived=False)
```

**Output**

```plaintext
[
    Subscription(
        id="f644efb3-8ba4-4bc4-8333-1ace54a3f111",
        aoi_id="fa200894-4e13-456c-872a-ba8efcda0812",
        dataset_id="f42f8f48-6cf9-48af-8453-be6cca46acb8",
        external_ref="123",
        created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
        created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
        archived_at=None,
        archived_by=None,
    ),
]
```

## Get subscription

This function allows you to get a subscription by ID, regardless of being archived.

**Input**

```python
client.get_subscription("f644efb3-8ba4-4bc4-8333-1ace54a3f111")
```

**Output**

```plaintext
Subscription(
    id="f644efb3-8ba4-4bc4-8333-1ace54a3f111",
    aoi_id="fa200894-4e13-456c-872a-ba8efcda0812",
    dataset_id="f42f8f48-6cf9-48af-8453-be6cca46acb8",
    external_ref="123",
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
    archived_at=None,
    archived_by=None,
)
```

## Archive subscription

This function allows you to archive a subscription by ID. When you archive a subscription, you have a grace period of 30 days to restore the subscription. Once the grace period is over, the subscription can no longer be restored from the archive, and the subscription data is permanently deleted.

**Input**

```python
client.archive_subscription("f644efb3-8ba4-4bc4-8333-1ace54a3f111")
```

**Output**

```plaintext
No content
```

## Restore subscription

This function allows you to restore a subscription by ID within the archive grace period.

**Input**

```python
client.restore_subscription("f644efb3-8ba4-4bc4-8333-1ace54a3f111")
```

**Output**

```plaintext
No content
```

# Data access

You can access Cecil datasets with the following functions. Additionally, you can develop interactive data visualisations with the [Earthscale integration](https://docs.cecil.earth/earthscale-integration).

| Function | Dataset type |
| --- | --- |
| Load xarray | `raster` |
| Load dataframe | `vector` |

## Load xarray

This function loads a `raster` dataset into an [xarray.Dataset](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html) for an active subscription. Raster datasets always have `x, y` dimensions and most times a `time` dimension. The following metadata is available in dataset-level attributes. See [datasets](https://docs.cecil.earth/datasets) for usage notes.

| Dimension | Type | Units |
| --- | --- | --- |
| `x` | Per dataset variable | Per CRS. |
| `y` | Per dataset variable | Per CRS. |
| `time` | datetime | Per dataset, when a time dimension is available. |

| Metadata |
| --- |
| `provider_name` |
| `dataset_name` |
| `dataset_id` |
| `aoi_id` |
| `subscription_id` |

**Input**

```python
client.load_xarray(
    subscription_id="f644efb3-8ba4-4bc4-8333-1ace54a3f111",
)
```

**Output**

```plaintext
<xarray.Dataset> Size: 3MB
Dimensions:        (x: 50, y: 50, time: 20)
Coordinates:
  * x              (x) float64 512B 68.57 68.57 68.57 ... 68.56 68.56
  * y              (y) float64 512B 45.78 45.78 45.78 ... 45.76 45.76 45.76
  * time           (time) datetime64[ns] 2kB 2021-03-21 ... 2025-12-21
Data variables:
    canopy_cover   (x, y, time) float64 3MB dask.array<chunksize=(50, 50, 1), meta=np.ndarray>
    canopy_height  (x, y, time) float64 3MB dask.array<chunksize=(50, 50, 1), meta=np.ndarray>
Attributes:
    provider_name:    Planet
    dataset_name:     Forest Carbon Monitoring
    dataset_id:       f42f8f48-6cf9-48af-8453-be6cca46acb8
    aoi_id:           fa200894-4e13-456c-872a-ba8efcda0812
    subscription_id:  f644efb3-8ba4-4bc4-8333-1ace54a3f111
```

## Load dataframe

This function loads a `vector` dataset into a [pandas.DataFrame](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) for an active subscription. Vector datasets have the following metadata in dataframe columns. See [datasets](https://docs.cecil.earth/datasets) for usage notes.

| Metadata |
| --- |
| `aoi_id` |
| `subscription_id` |

**Input**

```python
client.load_dataframe(
    subscription_id="f644efb3-8ba4-4bc4-8333-1ace54a3f111",
)
```

**Output**

```plaintext
     canopy_cover  timestamp
0            0.73  datetime.datetime(2021, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC))
1            0.79  datetime.datetime(2022, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC))
2            0.87  datetime.datetime(2023, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC))

[3 rows x 2 columns]
```

# Webhooks

Webhooks allow you to receive real-time notifications from the Cecil platform. The webhook `url` must use https, handle POST requests, and return a successful status (200-299) within 10 seconds. Failed requests will be retried 3 times at an interval of approximately 12 hours.

| Property | Type | Description |
| --- | --- | --- |
| `id` | uuid | Unique identifier. |
| `url` required | string | Your application URL to receive webhook events. |
| `secret` | string | Optional secret for signature validation. |
| `created_at` | datetime | Current system time when the webhook was created. |
| `created_by` | uuid | Authenticated user who created the webhook. |

## Create webhook

This function allows you to create a webhook.

**Input**

```python
client.create_webhook(
    url="https://api.example.com/cecil-webhook",
    secret="YT5MzU9xk3dhMDjZ",
)
```

**Output**

```plaintext
Webhook(
    id="fc9dbf41-a44c-4f13-8bac-1920dd98c39f",
    url="https://api.example.com/cecil-webhook",
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
)
```

## List webhooks

This function allows you to list all webhooks in your organisation.

**Input**

```python
client.list_webhooks()
```

**Output**

```plaintext
[
    Webhook(
        id="fc9dbf41-a44c-4f13-8bac-1920dd98c39f",
        url="https://api.example.com/cecil-webhook",
        created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
        created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
    ),
]
```

## Get webhook

This function allows you to get a webhook by ID.

**Input**

```python
client.get_webhook("fc9dbf41-a44c-4f13-8bac-1920dd98c39f")
```

**Output**

```plaintext
Webhook(
    id="fc9dbf41-a44c-4f13-8bac-1920dd98c39f",
    url="https://api.example.com/cecil-webhook",
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
)
```

## Delete webhook

This function allows you to delete a webhook by ID, stopping notifications immediately.

**Input**

```python
client.delete_webhook("fc9dbf41-a44c-4f13-8bac-1920dd98c39f")
```

**Output**

```plaintext
No content
```

## Signature validation

We recommend you to use the `Cecil-Signature` header to validate the authenticity and integrity of webhook events. Below is a code example in Python.

```python
import hashlib
import hmac

body = "request body received in the http request"
signature = "value from the Cecil-Signature header"
secret = "your-webhook-secret"

digest = hmac.new(
    digestmod=hashlib.sha256
    key=secret.encode("utf-8"),
    msg=body.encode("utf-8"),
).hexdigest()

if hmac.compare_digest(digest, signature):
    print("valid")
```

# Events

Webhook notifications include the following events.

| Event name | Event type |
| --- | --- |
| Subscription delivered | `subscription.delivered` |
| Subscription failed | `subscription.failed` |

## Subscription delivered

This event notifies that a data chunk has been delivered for a Subscription.

- For datasets with a time dimension, this includes data for a specific variable and time step.
- For datasets without a time dimension, this includes data for a specific variable and an explicit `null` value for the `payload.time` property.

**Example**

```plaintext
{
    "event_id": "f309e366-13dc-5f6c-8cad-c0c3c34bb548",
    "event_type": "subscription.delivered",
    "payload": {
        "aoi_id": "fa200894-4e13-456c-872a-ba8efcda0812",
        "subscription_id": "f644efb3-8ba4-4bc4-8333-1ace54a3f111",
        "dataset_id": "f42f8f48-6cf9-48af-8453-be6cca46acb8",
        "dataset_type": "raster",
        "time": "2026-01-09T00:00:00Z",
        "variable": "x_value"
    },
    "recorded_at": "2026-01-01T15:30:09.033Z"
}
```

## Subscription failed

This event notifies that a Subscription has failed to process. Possible failures are data not found for the specified AOI, or due to technical constraints from data providers, in which case, the `payload.error_message` contains the original error message from the data provider.

**Example**

```plaintext
{
    "event_id": "f309e366-13dc-5f6c-8cad-c0c3c34bb548",
    "event_type": "subscription.failed",
    "payload": {
        "aoi_id": "fa200894-4e13-456c-872a-ba8efcda0812",
        "subscription_id": "f644efb3-8ba4-4bc4-8333-1ace54a3f111",
        "dataset_id": "f42f8f48-6cf9-48af-8453-be6cca46acb8",
        "dataset_type": "vector",
        "error_message": "Data not found."
    },
    "recorded_at": "2026-01-01T15:30:09.033Z"
}
```

# Organisation settings

Organisation settings allow you to configure the following organisation-wide settings.

| Property | Type | Description |
| --- | --- | --- |
| `monthly_subscription_limit` | integer | Number of hectares across subscriptions that can be created per month. This value can only be configured for verified organisations. Please [contact us](https://cecil.earth/contact-us) to verify your organisation. Default: 50,000. |

## Get organisation settings

This function allows you to view your organisation settings.

**Input**

```python
client.get_organisation_settings()
```

**Output**

```plaintext
OrganisationSettings(
    monthly_subscription_limit=50_000,
)
```

## Update organisation settings

This function allows you to update your organisation settings.

**Input**

```python
client.update_organisation_settings(
    monthly_subscription_limit=50_000,
)
```

**Output**

```plaintext
OrganisationSettings(
    monthly_subscription_limit=50_000,
)
```

# User management

User management allows creating and viewing users in your organisation.

| Property | Type | Description |
| --- | --- | --- |
| `id` | uuid | Unique identifier. |
| `first_name` required | string | First name. |
| `last_name` required | string | Last name. |
| `email` required | string | Email address. |
| `created_at` | datetime | Current system time when the user was created. |
| `created_by` | uuid | Authenticated user who created the user. |

## Create user

This function allows you to create an organisation user. The new user will receive an email with instructions to generate their API key. The link sent via email expires in 7 days. If the link expires, the recover function can be used to generate their API key.

**Input**

```python
client.create_user(
    first_name="John",
    last_name="Smith",
    email="john.smith@greenfield.com",
)
```

**Output**

```plaintext
User(
    id="624e5cc3-aaf3-4b35-a92b-352f0d102daf",
    first_name="John",
    last_name="Smith",
    email="john.smith@greenfield.com",
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
)
```

## List users

This function allows you to list all users in your organisation.

**Input**

```python
client.list_users()
```

**Output**

```plaintext
[
    User(
        id="624e5cc3-aaf3-4b35-a92b-352f0d102daf",
        first_name="John",
        last_name="Smith",
        email="john.smith@greenfield.com",
        created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
        created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
    ),
]
```

## Get user

This function allows you to get a user by ID.

**Input**

```python
client.get_user("624e5cc3-aaf3-4b35-a92b-352f0d102daf")
```

**Output**

```plaintext
User(
    id="624e5cc3-aaf3-4b35-a92b-352f0d102daf",
    first_name="John",
    last_name="Smith",
    email="john.smith@greenfield.com",
    created_at=datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=TzInfo(UTC)),
    created_by="51f9cf7c-78f5-49e0-b6fd-ba281346ae3f",
)
```

# API key management

We recommend all users to automatically rotate their API keys on a regular basis. Users can also recover their API key via email.

## Rotate API key

This function allows users to rotate their API key. This is useful to improve your organisation security by implementing an automated process that rotates API keys on a regular basis.

**Input**

```python
client.rotate_api_key()
```

**Output**

```plaintext
RotateAPIKey(
    new_api_key="NzhjMWQwNjE1OGRhZDYxZDkxMWZlZjU1MWI1OTVlMmIK",
)
```

## Recover API key

This function allows users to recover their API key via email. If we find a user with the email address provided, the user will receive an email with instructions to recover their API key. The link sent via email expires in 7 days. This function doesn’t require authentication.

**Input**

```python
client.recover_api_key("jane.doe@example.com")
```

**Output**

```plaintext
RecoverAPIKey(
    message="Please follow the instructions sent via email.",
)
```


