How to publish STAC data at eodc

This guide explains how customers can publish and manage their own data in the eodc-hosted STAC API endpoint (https://cstac.discovery.eodc.eu). The typical workflow consists of preparing STAC metadata, authenticating to the API, and creating STAC Collections and Items.

Prerequisites

  • Valid eodc user credentials to authenticate against the STAC API.

  • Your dataset described using valid STAC metadata:

    • Collection – describes a dataset

    • Items – describe individual scenes or data entries

    • Assets – links to the actual data files

Example structure:

dataset/
 ├── collection.json
 └── items/
      ├── item_001.json
      ├── item_002.json
      └── ...

Ready-to-use sample data is available in the eodc-examples GitHub repository, if you want to try the workflow before ingesting your own data.

Access control

Some metadata fields are required by the STAC catalogue. In particular, the access field defines who can see the data.

Example: visible to all users in an organisation

{
  "access": {
    "visibility": "organisation",
    "allowed_users": [],
    "allowed_organisations": ["eodc"]
  }
}

Example: visible only to specific users

{
  "access": {
    "visibility": "user",
    "allowed_users": ["john.smith@example.org", "jane.smith@example.org"],
    "allowed_organisations": []
  }
}

Example: visible to everyone, managed by all users with an email address at example.org

{
  "access": {
    "visibility": "public",
    "allowed_users": [],
    "allowed_organisations": [],
    "allowed_domains": ["example.org"]
  }
}

Example: visible only to members of a group

{
  "access": {
    "visibility": "group",
    "allowed_groups": ["example-group"]
  }
}

Groups are set up by eodc on request. Your group memberships are included in your access token, so you may need to log in again after being added to a group.

visibility is one of public, organisation, user, domain or group. For organisation, user, domain and group, the listed organisations, users, email domains or groups can both see and manage the collection. A public collection is visible to everyone, but only the owners listed in allowed_organisations, allowed_users, allowed_domains or allowed_groups can change or delete it, so it needs at least one entry there.

The access block of a public collection is visible to everyone as well. Name its owners by allowed_groups, allowed_domains or allowed_organisations rather than by individual email addresses.

If the access field is missing or configured incorrectly, the data may not be visible in the catalogue.

Authenticate

from getpass import getpass
from eodc_connect.auth import EODCConnection
import requests

username = input("Enter your eodc username: ")
password = getpass("Enter your eodc password: ")

conn = EODCConnection(username=username, password=password)
token = conn.get_access_token()

HEADERS = {
    "Authorization": f"Bearer {token}",
    "Accept": "application/json",
    "Content-Type": "application/json",
}

Create a Collection

WRITE_BASE = "https://cstac.discovery.eodc.eu"

url = f"{WRITE_BASE}/collections"
r = requests.post(url, headers=HEADERS, json=collection)
r.raise_for_status()

Create Items

items_url = f"{WRITE_BASE}/collections/{collection_id}/items"

for item in items:
    r = requests.post(items_url, headers=HEADERS, json=item)
    r.raise_for_status()

Update an Item

Existing STAC Items can be updated using a PUT request:

  1. Retrieve the current Item with GET

  2. Modify the desired fields (e.g. metadata or assets)

  3. Replace the Item via PUT

item_url = f"{WRITE_BASE}/collections/{collection_id}/items/{item_id}"

r = requests.get(item_url, headers=HEADERS)
r.raise_for_status()
current = r.json()

current.setdefault("properties", {})
current["properties"]["updated"] = "2026-07-21T12:00:00Z"

r = requests.put(item_url, headers=HEADERS, json=current)
r.raise_for_status()

Bulk ingest Items

For larger batches, use the bulk endpoint instead of one request per Item. It writes many Items to the same Collection in a single request:

POST /collections/{collection_id}/bulk_items

The request body holds two fields:

  • items – a dictionary mapping each Item id to the full Item JSON

  • method – "insert" (default; fails if any Item already exists) or "upsert" (inserts new Items and replaces existing ones)

bulk_url = f"{WRITE_BASE}/collections/{collection_id}/bulk_items"

payload = {
    "items": {item["id"]: item for item in items},
    "method": "upsert",
}

r = requests.post(bulk_url, headers=HEADERS, json=payload)
r.raise_for_status()

All Items must belong to the Collection in the URL. If the endpoint returns 404 or 405, bulk ingest is not enabled; create the Items one by one as shown above instead.

Large Collections

For Collections with many thousands of Items, split the ingestion into batches:

  • Send 100–500 Items per request. Larger requests take longer and are more likely to time out.

  • Set a client timeout of at least 120 seconds, e.g. requests.post(..., timeout=120).

  • Use "method": "upsert", so that a batch can safely be sent again.

  • Retry failed batches (5xx responses or timeouts) after a short pause. A timeout does not necessarily mean that the batch failed, so check the Item count of the Collection afterwards.

  • Leave out item links in the Collection JSON. Items are linked automatically, and listing every Item in the Collection makes it very large and slow to load.

import time

def ingest(items, batch_size=200):
    for i in range(0, len(items), batch_size):
        batch = {item["id"]: item for item in items[i:i + batch_size]}
        for attempt in range(5):
            try:
                r = requests.post(bulk_url, headers=HEADERS, json={"items": batch, "method": "upsert"}, timeout=120)
                if r.status_code < 500:
                    r.raise_for_status()
                    break
            except requests.exceptions.Timeout:
                pass
            time.sleep(10 * (attempt + 1))
        else:
            raise RuntimeError(f"Batch starting at item {i} failed")

To check the number of Items in a Collection:

r = requests.get(f"{WRITE_BASE}/search", headers=HEADERS, params={"collections": collection_id, "limit": 1})
print(r.json()["numberMatched"])

Delete a Collection

del_url = f"{WRITE_BASE}/collections/{collection_id}"
r = requests.delete(del_url, headers=HEADERS)
r.raise_for_status()

Common mistakes

Missing access metadata — if the access field is missing, the dataset may not be visible to other users.

Invalid STAC structure — make sure the metadata follows the STAC specification. The stac-validator tool can help catch issues before ingestion:

stac-validator collection.json

Incorrect asset links — assets must point to valid and accessible data files.

Troubleshooting

Item creation fails — check that the collection exists, item IDs are unique, and the metadata is valid JSON.

Data not visible in the catalogue — check the access settings, organisation name, and permissions.

Validation errors — run a STAC validator before ingestion.

Support

If you encounter problems publishing STAC data at eodc, please contact support@eodc.eu.

Worked example

See the Customer STAC ingestion notebook for a full runnable walkthrough — authenticating, loading sample data, creating a Collection and Items, updating an Item, bulk ingesting Items, and cleaning up.