# Crossref DOI Metadata Normalizer (`monlab/crossref-metadata-normalizer`) Actor

Normalize DOI lists and Crossref bibliographic searches into clean metadata, with duplicate and error reports.

- **URL**: https://apify.com/monlab/crossref-metadata-normalizer.md
- **Developed by:** [monlab](https://apify.com/monlab) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 normalized metadata records

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

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

### What does Crossref DOI Metadata Normalizer do?

Turn **DOI lists and bibliographic searches** into consistent JSON or CSV using the [official Crossref API](https://api.crossref.org/). Get one successful record per normalized DOI, with a separate report for duplicate inputs, invalid DOIs, missing records, and upstream errors.

### Why use this Actor?

Import references into a spreadsheet, join DOI lists to publication metadata, or feed a research workflow. Resolver links and mixed-case DOIs are normalized. Each result keeps its source and retrieval time. Partial publication dates retain their original precision: a year stays a year, without an invented January 1 date.

Apify provides API access, downloadable datasets, integrations, run reports, and scheduling. This Actor uses deterministic processing and ordinary API calls. It requires no LLM, browser, or proxy.

### What data can it extract?

| Fields | Type | Meaning |
| --- | --- | --- |
| `doi`, `doiUrl`, `sourceUrl` | string | Normalized identifier and official links |
| `title`, `containerTitle`, `publisher`, `type` | string or null | Registered bibliographic information |
| `publishedDate`, `publishedDatePrecision`, `publishedDateParts` | string, string, array | Date and its original precision |
| `printPublishedDate`, `onlinePublishedDate` | string or null | Print and online dates, which may differ |
| `issn`, `licenseUrls` | array | ISSNs and registered licence links |
| `citationCount` | integer or null | Crossref referenced-by count; coverage is incomplete |
| `inputIndexes` | integer array | One-based matching input positions; empty in search mode |
| `retrievedAt`, `schemaVersion`, `missingFields` | string, string, array | Retrieval time, output version, unavailable core fields |

This is a whitelist of 19 fields. Abstracts, full text, author records, and author contact details are omitted. Nulls and empty arrays represent unavailable metadata.

### How to retrieve Crossref metadata

1. Choose **DOI list** or **Bibliographic search** in the Input tab.
2. In DOI mode, enter DOI strings or doi.org links and leave query and dates empty. In search mode, leave DOIs empty and enter a query, optionally with publication dates.
3. Set the maximum number of successful results and start the run.
4. Open the dataset and the separate report. Datasets can be downloaded as JSON, HTML, CSV, or Excel.

DOI input:

```json
{"mode":"doi","dois":["10.1038/nphys1170","https://doi.org/10.1038/NPHYS1170"],"maxResults":10}
```

Search input:

```json
{"mode":"search","query":"solar energy","fromDate":"2025-01-01","toDate":"2025-12-31","maxResults":100}
```

### How much does it cost?

**$1 per 1,000 successful unique records** ($0.001 each), plus **$0.00005 per run start**. Apify platform usage is billed separately to the user. Memory is fixed at 256 MB, so the start event is charged once per run. Invalid or missing DOIs and duplicate inputs do not create result charges.

For example, 100 successful records cost $0.10005 in event fees, plus platform usage. A run with no successful records still incurs the start fee and platform usage. The minimum selectable event spending limit is $0.01; it is a limit, not a minimum charge.

Waiting and API retries contribute to execution time. Small result sets and empty searches may cost more per successful result than batched searches. Start with a small result limit. Successful dataset items will be charged; errors and duplicate inputs do not create dataset items, but still incur platform usage. The code stops when the SDK reports that the event spending limit has been reached. The event spending limit should not be treated as a guarantee that separately billed platform usage is included.

### Input options and limits

See the Input tab for configuration. `maxResults` defaults to 100, with a hard maximum of 1,000. DOI lists contain at most 200 entries, including invalid and duplicate entries. Search date filters use inclusive `YYYY-MM-DD` publication dates, not deposit dates. Unknown input fields are rejected.

Requests are sequential, with at least two seconds after the preceding response. The hard request limit is 200, including retries. Network errors, HTTP 429 and 5xx are retried up to three total attempts. Retry-After is honored. The Actor stops instead of waiting longer than two minutes. HTTP 403 and redirects stop the run. Limits cannot be raised through inputs. Other workloads sharing an API pool may still cause throttling.

Memory is fixed at 256 MB to keep platform costs predictable. The default run timeout is 20 minutes.

Optional `contactEmail` is encrypted in Apify input and forwarded to Crossref as its `mailto` contact parameter. Use a business contact if desired. Empty uses the public pool. The contact is excluded from output, reports, and application logs.

### Output and recovery

The dataset contains successful unique metadata records. `REPORT` in the key-value store gives result/request counts, duplicate counts, invalid/missing DOI indexes and the stop reason. Invalid or missing DOIs can leave a successful run with fewer results than inputs; check the report.

Simplified output example:

```json
[
  {"doi":"10.1234/example.001","publishedDate":"2025","publishedDatePrecision":"year","inputIndexes":[1,3]},
  {"doi":"10.1234/example.002","publishedDate":"2025-02","publishedDatePrecision":"month","inputIndexes":[2]}
]
```

Progress is checkpointed to `STATE` after each DOI or search page and during graceful abort. Resurrection uses progress within the same run storage. Dataset and state writes are separate operations; recovery is best effort, so downstream workflows should also deduplicate by DOI. Fresh runs fetch data anew. There is no cache across separate runs.

### FAQ and source conditions

#### Why are some dates or titles missing?

Publisher deposits can be incomplete. Missing core fields are listed in `missingFields`.

#### Does a licence link allow reuse of an abstract?

Not necessarily. Abstracts and full text are omitted. Read the [Crossref metadata policy](https://www.crossref.org/documentation/retrieve-metadata/rest-api/) and [API rate limits](https://www.crossref.org/blog/announcing-changes-to-rest-api-rate-limits/). This Actor is independent of Crossref and does not verify publisher metadata accuracy.

#### What happens if access is denied?

A safe diagnostic is reported and the run stops. The Actor does not rotate proxies or bypass restrictions. The API tab provides programmatic access; the Issues tab can collect feedback after publication.

### Local development

Python 3.12, Apify SDK 4.0.2 and HTTPX 0.28.1 are pinned with resolved dependencies in `requirements-lock.txt`. Run locally via the workspace `scripts/run_actor.py` helper or official `apify run` with the workspace virtual environment. Validate Console schemas with `apify validate-schema`. The standard-library unittest suite covers dates, errors, request limits, duplicates, graceful abort and event spending limits.

# Actor input Schema

## `mode` (type: `string`):

Look up DOI lists or search bibliographic metadata.

## `dois` (type: `array`):

DOI mode: up to 200 DOI strings or doi.org URLs. Search mode: leave empty.

## `query` (type: `string`):

Search mode only: a bibliographic query. DOI mode: leave empty.

## `fromDate` (type: `string`):

Search only. Optional inclusive publication date in YYYY-MM-DD.

## `toDate` (type: `string`):

Search only. Optional inclusive publication date in YYYY-MM-DD.

## `maxResults` (type: `integer`):

Maximum successful unique results. Hard maximum 1000; DOI lists have at most 200 entries.

## `contactEmail` (type: `string`):

Optional business contact forwarded to Crossref as mailto. Encrypted by Apify; not included in logs or reports. Empty uses the public pool.

## Actor input object example

```json
{
  "mode": "doi",
  "dois": [
    "10.1038/nphys1170"
  ],
  "query": "",
  "fromDate": "",
  "toDate": "",
  "maxResults": 100
}
```

# Actor output Schema

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

Successful unique DOI records.

## `report` (type: `string`):

Counts and safe error codes.

# 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 = {
    "dois": [
        "10.1038/nphys1170"
    ],
    "contactEmail": ""
};

// Run the Actor and wait for it to finish
const run = await client.actor("monlab/crossref-metadata-normalizer").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 = {
    "dois": ["10.1038/nphys1170"],
    "contactEmail": "",
}

# Run the Actor and wait for it to finish
run = client.actor("monlab/crossref-metadata-normalizer").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 '{
  "dois": [
    "10.1038/nphys1170"
  ],
  "contactEmail": ""
}' |
apify call monlab/crossref-metadata-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,monlab/crossref-metadata-normalizer"
        }
    }
}
```

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/rviz0dTzLogQfTo0m/builds/naWIXidOiEd58rJzi/openapi.json
