# Open Herbarium Specimens & Species Distribution (`grit-77/gbif-herbarium-specimens`) Actor

GBIF herbarium specimen search for preserved plants. Filter by scientific name, dataset and country; export CC0 specimen and taxon metadata as JSON with dataset DOI attribution. No coordinate maps.

- **URL**: https://apify.com/grit-77/gbif-herbarium-specimens.md
- **Developed by:** [Grit](https://apify.com/grit-77) (community)
- **Categories:** Open source, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.20 / 1,000 specimen

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Open Herbarium Specimens & Species Distribution

Search CC0 preserved plant specimens from GBIF by scientific name, dataset and country, with minimal taxon facts and dataset DOI/source attribution.

Use it to build a herbarium inventory or a country/year specimen table. Distribution here means the country reported for a specimen; coordinates and maps are outside this actor's output.

### Data source and license handling

The actor uses the official [GBIF occurrence search API](https://techdocs.gbif.org/en/openapi/v1/occurrence) and [dataset metadata API](https://techdocs.gbif.org/en/openapi/v1/registry). Occurrence searches set `basisOfRecord=PRESERVED_SPECIMEN`, `license=CC0_1_0` and `kingdomKey=6`, including later pages and retries. The actor checks each record's license, record type and plant kingdom before export. Dataset metadata must also report CC0; conflicting, noncommercial or unknown licenses are excluded.

Records contain the fields below. Collector and identifier names, author strings, usernames, contacts, remarks, coordinates, media and raw source payloads are excluded from the result projection. Source links are generated from validated identifiers. The linked GBIF pages themselves may contain additional source fields.

### What it returns

| Field | Meaning |
|---|---|
| `key` | Numeric GBIF occurrence record key |
| `datasetKey` | GBIF source dataset UUID |
| `datasetDoi` | Dataset DOI from official metadata; `null` when absent or invalid |
| `genus` | GBIF genus component, without authorship; `null` if missing or malformed |
| `specificEpithet` | GBIF specific epithet component, without authorship; `null` if missing or malformed |
| `taxonKey` | Numeric GBIF taxon key |
| `country` | Reported two-letter country code; `null` when absent |
| `year` | Reported collection year; `null` when absent or invalid |
| `basisOfRecord` | `PRESERVED_SPECIMEN` |
| `sourceUrl` | GBIF occurrence page |
| `datasetUrl` | GBIF dataset page |
| `datasetApiUrl` | Official dataset metadata endpoint |
| `datasetDoiUrl` | Dataset DOI resolver link; `null` when no valid DOI is available |
| `publishingOrganizationKey` | GBIF publishing organization UUID; `null` when absent |
| `hostingOrganizationKey` | GBIF hosting organization UUID, when supplied; otherwise `null` |
| `owningOrganizationKey` | GBIF owning organization UUID, when supplied; otherwise `null` |

Organizational UUIDs identify organizations in GBIF metadata; they do not establish legal ownership. Personal contact records and arbitrary metadata identifiers are excluded.

### Input

The supplied Store prefill is:

```json
{
  "scientificName": "Quercus robur",
  "datasetKey": "857bce66-f762-11e1-a439-00145eb45e9a",
  "maxItems": 10
}
```

| Input | Default | Description |
|---|---|---|
| `scientificName` | Unset | Plant scientific name; GBIF applies its taxonomic search behavior |
| `datasetKey` | Unset | Optional dataset UUID |
| `country` | Unset | Optional two-letter ISO country code, for example `DE` |
| `maxItems` | 100 | Accepted result cap, from 1 to 10,000; Store prefill is 10 |
| `concurrency` | 4 | Maximum concurrent GBIF requests, from 1 to 10 |

At least one of `scientificName`, `datasetKey` or `country` is required. Filters combine. Add `"country": "DE"` to the prefill to restrict it to Germany. The actor does not accept overrides for license or record type. Store prefill values are examples, not runtime defaults: empty input fails with a clear status.

### Actual output example

This row is from the saved real prefill run in `sample_output.json`:

```json
{
  "key": 5795615752,
  "datasetKey": "857bce66-f762-11e1-a439-00145eb45e9a",
  "datasetDoi": "10.15468/ynjblx",
  "genus": "Quercus",
  "specificEpithet": "robur",
  "taxonKey": 2878688,
  "country": "DE",
  "year": 2025,
  "basisOfRecord": "PRESERVED_SPECIMEN",
  "sourceUrl": "https://www.gbif.org/occurrence/5795615752",
  "datasetUrl": "https://www.gbif.org/dataset/857bce66-f762-11e1-a439-00145eb45e9a",
  "datasetApiUrl": "https://api.gbif.org/v1/dataset/857bce66-f762-11e1-a439-00145eb45e9a",
  "datasetDoiUrl": "https://doi.org/10.15468/ynjblx",
  "publishingOrganizationKey": "2d6e0e40-2aee-11d8-aa2d-b8a03c50a862",
  "hostingOrganizationKey": null,
  "owningOrganizationKey": null
}
```

### Pricing proposal

`specimen`: **$0.0002 per accepted exported row ($0.20 per 1,000)**. Rejected records and safe per-item failure rows do not trigger this event; dataset DOI lookups have no extra event. This is an unpublished proposal. The [current-activity comparison leader](https://apify.com/parseforge/gbif-occurrence-search-scraper) was $0.024/result on the Free tier and $0.018/result on its lowest listed tiers when checked on 2026-10-06. The lifetime-run leader was $0.003/result plus a start fee. See [PRICING.md](PRICING.md) and its saved Store API evidence for the selection method and other comparables.

### Limits and failures

- The actor exports at most 10,000 accepted records per run and deduplicates GBIF record keys within that run.
- GBIF search supports pages of at most 300 records and a 100,000-record search window. The actor respects these bounds and reports `search_window` if it reaches that window before the requested accepted count. For larger inventories, use GBIF's download service separately. [GBIF search limits](https://techdocs.gbif.org/en/openapi/v1/occurrence).
- Network failures, HTTP 429/5xx and malformed JSON have at most four attempts, with exponential backoff and a maximum 30-second delay. Permanent HTTP errors are not retried.
- Dataset resolution is cached for one run, including failed resolutions. Unresolvable datasets and malformed record identifiers are skipped; other valid records continue.
- Safe per-item failure rows are saved in the key-value record `ERRORS`, with only `key`, `datasetKey` and a fixed `errorCode`. They are kept outside the result dataset to preserve its field restriction.
- `SUMMARY` reports exported/skipped/duplicate counts, errors, request counts, duration and the stop reason. A terminal occurrence-search error fails the run after saving these records; previously exported rows remain available.
- Missing DOI, year, country, genus/epithet or optional organization UUIDs can be `null`. Invalid genus/epithet strings are omitted rather than falling back to names with authorship.
- GBIF inventories change during pagination; repeated runs are not permanent snapshots. A country/year inventory measures reported records, not population size, complete geographic range or absence of a species.
- Cloud Docker build, platform costs and real billing settlement are unverified. The local package uses `apify/actor-python:3.12` and the installed SDK was exercised locally.

### Local run and checks

No Apify CLI or account is required for the local run. From this actor directory, use the owner's existing environment:

```powershell
& C:/GritWork/earn/.venv/Scripts/python.exe tools/run_local.py
& C:/GritWork/earn/.venv/Scripts/python.exe tools/run_checks.py unit
& C:/GritWork/earn/.venv/Scripts/python.exe tools/run_checks.py live
apify validate-schema
```

`run_local.py` writes the exact prefill to `storage/key_value_stores/default/INPUT.json`, sets `APIFY_LOCAL_STORAGE_DIR` to this actor's `storage` directory and launches `python -m src`. To supply another query, write that INPUT.json yourself, set the same storage environment variable and run `C:/GritWork/earn/.venv/Scripts/python.exe -m src` directly. No additional venv packages were installed; runtime requirements are in `requirements.txt`.

Evidence and the independent source comparisons are retained in `evidence/`. Unit tests use projections saved from real responses, with adversarial mutations for license, authorship and error cases. The marked live smoke test contacts public GBIF endpoints. Local PPE probes simulate events without calling paid cloud APIs.

### FAQ

**Can I search without a dataset?** Yes. Supply a scientific name or country. Supplying both name and dataset narrows the inventory.

**Why are there fewer results than GBIF shows?** This actor restricts the search to preserved plants under CC0, then rechecks every row and dataset. Invalid, duplicate and unresolved rows can reduce the count. Empty results are valid.

**Does it provide a map or GPS points?** It returns country and year only. Coordinates and media are excluded.

**How do I attribute the source?** Keep `datasetKey`, `datasetDoi`, `datasetDoiUrl`, `datasetUrl` and `sourceUrl` with your exported records. GBIF's organization UUIDs provide registry identifiers rather than personal names.

**Can I request CC BY or CC BY-NC records?** The actor accepts only recognized CC0 licenses. License overrides are rejected.

**Why is `charged` zero in an ordinary local run?** The SDK ignores billing when PPE is not configured locally. The actor still calls the specimen event; controlled local PPE tests verify event counts and budget stops. Local simulations do not charge an account.

### Use with AI agents / MCP

After the owner publishes the actor, an agent can call it through Apify's actor tools or [Apify MCP server](https://docs.apify.com/platform/integrations/mcp), passing the JSON input above, then read the default dataset. A useful request is: “Get up to ten CC0 Quercus robur preserved specimens from this dataset, retain the dataset DOI, and group them by country and year.”

Use `key` for deduplication and preserve the dataset/source links in agent answers. Treat the country field as a reported specimen location; avoid inferring a complete species range. This package has not been published, so it does not yet have a live Store actor ID or MCP tool registration.

# Actor input Schema

## `scientificName` (type: `string`):

Plant scientific name, for example Quercus robur. Optional when datasetKey or country is supplied. GBIF performs the taxonomic search.

## `datasetKey` (type: `string`):

Optional GBIF dataset UUID. Combine with scientificName or country to narrow the specimen inventory.

## `country` (type: `string`):

Optional two-letter ISO country code such as DE, GB or TR. Restricts specimens to their reported country.

## `maxItems` (type: `integer`):

Maximum accepted specimen rows to export and charge. Default 100; hard cap 10000. Prefill uses 10.

## `concurrency` (type: `integer`):

Limit simultaneous official GBIF requests. Dataset lookups are cached within the run; search pages are fetched in order.

## Actor input object example

```json
{
  "scientificName": "Quercus robur",
  "datasetKey": "857bce66-f762-11e1-a439-00145eb45e9a",
  "maxItems": 10,
  "concurrency": 4
}
```

# Actor output Schema

## `dataset` (type: `string`):

Accepted preserved plant specimen rows with dataset identifiers and source links.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "scientificName": "Quercus robur",
    "datasetKey": "857bce66-f762-11e1-a439-00145eb45e9a",
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("grit-77/gbif-herbarium-specimens").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "scientificName": "Quercus robur",
    "datasetKey": "857bce66-f762-11e1-a439-00145eb45e9a",
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("grit-77/gbif-herbarium-specimens").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "scientificName": "Quercus robur",
  "datasetKey": "857bce66-f762-11e1-a439-00145eb45e9a",
  "maxItems": 10
}' |
apify call grit-77/gbif-herbarium-specimens --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,grit-77/gbif-herbarium-specimens"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/0Ez9tS0Avp1YhpIqT/builds/XSgmhTd2o95jDbsHw/openapi.json
