# eIDAS Signature Validator: Check Signed PDFs & XML (QES, EU) (`lerider/eidas-signature-check`) Actor

Is this signed PDF genuine? Validates PDF signatures and e-seals against the official EU trusted lists: intact, signer, qualified (QES) or not, trust provider, changed after signing. ALLOW / REVIEW / BLOCK verdict for contracts, invoices and AI agents.

- **URL**: https://apify.com/lerider/eidas-signature-check.md
- **Developed by:** [Lerider Technologies](https://apify.com/lerider) (community)
- **Categories:** Business, Developer tools, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $30.00 / 1,000 signed pdf checkeds

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

## eIDAS Signature Validator: Check Signed PDFs & XML (QES, EU)

**Is this signed document genuine, and can I rely on it?** This Actor validates the digital signatures and electronic
seals in **PDFs**, in **signed XML** (XAdES / XML-DSig) and in **separate signature files** (detached XAdES) against
the **official EU trusted lists** (eIDAS), the way an EU validation service does, and returns one verdict per
document: `ALLOW`, `REVIEW` or `BLOCK`, with the reasons.

**New: authentic EU law.** Give a CELEX number or ELI (e.g. `32024R1689`, the AI Act) and the Actor fetches the
act's official PDF and the Publications Office's qualified e-seal and proves the text is the authentic Official
Journal version, in any of the 24 languages. Or give your own copy and find out whether it is exactly the
published text.

Built for AI agents and automations that receive signed contracts, mandates, invoices, certificates or official
documents and cannot "look at the blue signature bar" themselves; and for KYB/onboarding, procurement and legal ops.

### What it checks, per signature

- **Intact:** the signed bytes still match the signature (no changes inside what was signed)
- **Changed after signing:** pages, page content or resources added or replaced later (a classic way to slip a
  different text under a valid signature), versus allowed changes such as form filling, annotations and
  long-term-validation data
- **Byte-range tricks:** signatures that don't cover their whole revision
- **Who signed:** signer and issuing CA (name, organisation, country)
- **EU trust:** does the signer's certificate chain to a trust service on an **EU member state trusted list**
  (all 27 + EEA lists, via the EU List of Trusted Lists, signatures of the lists verified)?
- **Qualified or not:** qualified electronic signature (QES) or qualified seal, from the trusted list and the
  certificate's QC statements, plus whether the key is on a qualified device (QSCD), and the trust service's name
- **Revocation:** OCSP/CRL checked where the certificate points to them
- **Signing time:** claimed time and the time proven by a trusted timestamp (long-term validation, PAdES B-T/LT/LTA)

### Verdicts

| Verdict | When |
|---|---|
| `ALLOW` | Every signature is intact, cryptographically valid, chains to an EU trusted provider, not revoked, and the pages were not changed after signing. Set `require_qualified: true` to also require a qualified signature or seal |
| `REVIEW` | Intact, but trust can't be fully established: signer not under an EU trusted provider (e.g. a non-EU or self-made certificate), validity can't be proven at signing time, revocation info unavailable, forms or annotations changed, or not qualified while you asked for qualified |
| `BLOCK` | Signature broken or doesn't match the content, certificate revoked, pages or page content changed after signing, byte-range manipulation, unreadable PDF, or no signature at all |

`REVIEW` and `BLOCK` both mean "don't proceed automatically"; `BLOCK` means there is evidence the document is not
what was signed.

### Separate signature files and EU Official Journal acts

Many signed documents keep the signature in a separate `.xml` file (detached XAdES): contracts signed with national
eID tools, e-invoicing platforms, archives, and every **EU Official Journal act since October 2023** (one qualified
seal of the Publications Office covers the act in all 24 languages). The Actor:

- verifies the XML signature itself (signed info, every reference, the XAdES signing-certificate binding),
- checks that **your file matches one of the files the signature covers** (`document_match`; a changed file, or a
  different language version, is `BLOCK`),
- validates the signer's chain against the EU trusted lists and its qualified status,
- validates the **signature timestamp** (RFC 3161, timestamp authority on an EU trusted list) and, when the
  certificate has expired since, proves validity **at the timestamp** with the revocation data embedded in the
  signature (long-term validation, XAdES-LT/LTA; `sub_indication: OK_AT_TIMESTAMP`).

Signed XML on its own (e.g. a signed XML invoice or declaration) is checked as an enveloped signature; a signature
that covers only part of the XML gets `REVIEW`, so a signed fragment can't vouch for unsigned content around it.

### Pricing

| Event | Price | When |
|---|---|---|
| `signature-check` | **$0.03** | A document was read and checked: PDF, signed XML, document + separate signature, or EU act (signed or unsigned: "no signature" is an answer too) |
| (free) | $0 | URL could not be downloaded, file is not a readable PDF/XML, EU act not found or without a seal, malformed input |

### Input

```json
{"url": "https://example.com/signed-contract.pdf", "require_qualified": true}
```

```json
{"documents": [{"url": "https://example.com/a.pdf"}, {"pdf_base64": "JVBERi0xLjc...", "name": "mandate.pdf"}]}
```

```json
{"url": "https://example.com/contract.pdf", "signature_url": "https://example.com/contract.xml"}
```

```json
{"eu_act": "32024R1689", "language": "DE", "require_qualified": true}
```

```json
{"documents": [{"eu_act": "http://data.europa.eu/eli/reg/2024/1689/oj", "language": "NL",
                "pdf_base64": "JVBERi0xLjc... (your own copy)"}]}
```

Up to 20 documents per run, 25 MB each. Clear the single `url` field when you send `documents`. With `eu_act`, the
output also has `eu_act` (CELEX, language and the Publications Office links of the PDF and the seal).

### Output (one item per document; illustrative values, shortened)

```json
{
  "index": 0,
  "source": "https://example.com/signed-contract.pdf",
  "verdict": "ALLOW",
  "status": "checked",
  "reasons": ["Signature1: valid qualified signature, signer chains to an EU trusted provider (Example Qualified CA)"],
  "signatures": [{
    "field": "Signature1", "verdict": "ALLOW", "indication": "PASSED", "sub_indication": "OK",
    "signer": {"common_name": "…", "organization": "…", "country": "BE"},
    "issuer": {"common_name": "Example Qualified CA", "country": "BE"},
    "claimed_signing_time": "…", "proven_signing_time": "…",
    "intact": true, "crypto_valid": true, "trusted": true, "revoked": false,
    "qualified": true, "qualified_type": "qualified_signature", "qscd": true,
    "trust_service": "Example Qualified CA service",
    "coverage": "ENTIRE_FILE", "changed_after_signing": null, "timestamped": true
  }],
  "document": {"sha256": "…", "bytes": 61234, "signature_count": 1, "document_timestamps": 0},
  "trusted_lists": {"built_at": "2026-10-28T19:00:00+00:00", "source": "baked"}
}
```

`indication` / `sub_indication` follow ETSI EN 319 102-1 (`PASSED`, `INDETERMINATE`, `FAILURE`). A valid signature
without a signature timestamp passes with `sub_indication: OK_NO_TIMESTAMP`: its validity is proven as of the check,
not as of signing.

### Scope and limits (v0.2)

- **PDF signatures** (PAdES and Adobe-style PDF signatures, incl. document timestamps), **XML signatures**
  (XAdES-B/T/LT/LTA and plain XML-DSig, enveloped or detached, direct references or a signed `ds:Manifest`). ASiC
  containers and CAdES (.p7m) are not supported yet. For an XML file with several signatures, the first is validated
  and the others are listed as not validated (`REVIEW`).
- **XML archive timestamps** (XAdES-LTA) are counted (`archive_timestamps`) but not verified; validity rests on the
  signature timestamp and the embedded revocation data.
- **EU acts:** only Official Journal acts published act-by-act from 1 October 2023 carry a detached seal. Older acts
  and consolidated versions have none; the Actor says so and doesn't charge.
- **Legacy formats:** signatures in formats the engine can't validate (e.g. the SHA-1 based `adbe.pkcs7.sha1` some
  official gazettes still use) are reported as present, with signer and issuer, and get `REVIEW`, never "unsigned".
- **Advanced vs. qualified:** many official e-seals (e.g. Austria's Amtssignatur) come from CAs that are not on an EU
  trusted list; they are intact and cryptographically valid but get `REVIEW` because they are not EU-qualified.
- **EU trust only:** a signature under a non-EU CA (e.g. a US e-signature platform's own certificate) gets `REVIEW`,
  not `BLOCK`: it may be fine, it just isn't EU-trusted.
- Some historic services on national lists use extensions the engine can't read; signatures under them get
  `REVIEW`, never a false `ALLOW`.
- **How current are the trusted lists?** The EU List of Trusted Lists and every national list are downloaded,
  signature-checked and bundled into this Actor **every Sunday at about 05:30 Amsterdam time**, after the test
  suite passes. Every result shows when (`trusted_lists.built_at`). If the bundled copy is ever older than 7 days,
  the run downloads fresh lists itself (+15–30 s), so a result never relies on trusted lists older than a week.
  Revocation of the signer's certificate (OCSP/CRL) is not taken from the bundle: it is checked during your run,
  from the revocation data in the document and, where needed, the certificate authority's live service.
- A technical validation, not legal advice on what a signature means for your contract.

Engine: [pyHanko](https://github.com/MatthiasValvekens/pyHanko) (MIT) and, for XML,
[signxml](https://github.com/XML-Security/signxml) (Apache-2.0), tested against attack and edge-case files from the
European Commission's DSS test suite, signature-wrapping and entity-expansion attacks, and real Official Journal
seals. Revocation and issuer lookups only go to public internet addresses. Documents are processed in memory for your run only; nothing is stored
beyond your run's dataset.

### Related

- [Payee Check](https://apify.com/lerider/payee-check): IBAN, VAT, Peppol and invoice checks before paying a supplier
- [EU Financial License Check](https://apify.com/lerider/eu-licence-check): is this firm authorised or on a warning list?
- [EU VAT Validator](https://apify.com/lerider/eu-vat-validator) · [EORI Validator](https://apify.com/lerider/eori-validator) · [EU TIN Validator](https://apify.com/lerider/eu-tin-validator)

Made by Lerider Technologies (lerider.com).

# Changelog

This Actor's version history is a separate document: https://apify.com/lerider/eidas-signature-check/changelog.md

# Actor input Schema

## `url` (type: `string`):

Link to a signed PDF, a signed XML file, or (with a signature file below) any signed document (max 25 MB). Leave empty when you use 'Document (base64)', 'EU act' or 'Documents'.

## `pdf_base64` (type: `string`):

The document as base64, instead of a URL (PDF, signed XML, or any file that has a separate signature file).

## `signature_url` (type: `string`):

For a detached XML signature (XAdES / XML-DSig, e.g. a .xml or .sig next to a contract, or an EU Official Journal seal): link to the signature file. The document above is then compared with what the signature covers.

## `signature_base64` (type: `string`):

The detached XML signature as base64, instead of 'Separate signature file URL'.

## `eu_act` (type: `string`):

Check an EU Official Journal act published from October 2023: CELEX number (e.g. 32024R1689) or ELI (e.g. http://data.europa.eu/eli/reg/2024/1689/oj). The Actor fetches the act's official seal (and the PDF, unless you give your own copy as the document) from the EU Publications Office.

## `language` (type: `string`):

Language version of the EU act (only used with 'EU act').

## `require_qualified` (type: `boolean`):

If true, a valid but non-qualified (advanced) signature gets REVIEW instead of ALLOW.

## `documents` (type: `array`):

Up to 20 per run: \[{"url": "..."}, {"pdf_base64": "...", "name": "contract.pdf"}, {"url": "contract.pdf link", "signature_url": "contract.xml link"}, {"eu_act": "32024R1689", "language": "DE"}]. Clear the single fields above when you use this.

## Actor input object example

```json
{
  "url": "https://raw.githubusercontent.com/esig/dss/master/dss-pades/src/test/resources/validation/pdf-signed-added-page.pdf",
  "language": "EN",
  "require_qualified": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `overview` (type: `string`):

No description

# 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 = {
    "url": "https://raw.githubusercontent.com/esig/dss/master/dss-pades/src/test/resources/validation/pdf-signed-added-page.pdf"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lerider/eidas-signature-check").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 = { "url": "https://raw.githubusercontent.com/esig/dss/master/dss-pades/src/test/resources/validation/pdf-signed-added-page.pdf" }

# Run the Actor and wait for it to finish
run = client.actor("lerider/eidas-signature-check").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 '{
  "url": "https://raw.githubusercontent.com/esig/dss/master/dss-pades/src/test/resources/validation/pdf-signed-added-page.pdf"
}' |
apify call lerider/eidas-signature-check --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lerider/eidas-signature-check"
        }
    }
}
```

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/MPErEYjYs1jsfRdVm/builds/9ejwOjSynLOIYX5ic/openapi.json
