Constaia
Use-case guides

Sexual offences certificate (LOPIVI)

Validate the Spanish sex offender registry clearance of coaches and volunteers (recent, right holder, no records) without storing it.

If people in your organisation work with minors (coaches, instructors, volunteers), Spanish law (LOPIVI) requires you to ask them for the clearance certificate from the Central Sex Offender Registry (Registro Central de Delincuentes Sexuales). With the es_sexual_offences_certificate type Constaia checks that it is that certificate, that it belongs to that person, that it is not old and that it states there are no records.

This is especially sensitive data, so this guide processes it without storing anything at Constaia.

This guide explains how to automate the check, not what the law requires in your case. The maximum age you accept and how often you ask for it again are decided by your organisation with its legal counsel.

Options

options.json
{
  "expect": "es_sexual_offences_certificate",
  "checks": {
    "max_age_days": 90,
    "holder": { "full_name": "María García López", "document_number": "12345678Z" }
  },
  "storage": "none",
  "keep_results": false,
  "metadata": { "volunteer_id": "311" }
}
OptionWhy
expectIf another document arrives (for example the criminal record certificate, which is es_criminal_record_certificate), the verdict is invalid with type_mismatch.
checks.max_age_daysMaximum age since the issue date. 90 is an example: use the window your organisation has set.
checks.holderThat the certificate holder is the person you have on record.
storage: "none"The file is processed in memory and never written to any storage.
keep_results: falseExtracted data isn't stored either. You get the response once and afterwards GET /v1/analyses/{id} returns 404.

The has_records reason is always added, without asking:

has_recordsseverityMessage
The certificate states there are no recordsinfo"The certificate states that there are no records."
The certificate states there are recordserror → invalid"The certificate states that there are records."

The verification code (CSV)

These certificates carry a secure verification code (CSV, "código seguro de verificación"). Constaia extracts it in csv_code and runs the csv_format validation.

csv_format only checks the format

csv_format checks that the code has a valid format. Constaia does not ask the Ministry of Justice whether the code exists or matches this certificate. If your procedure requires checking authenticity, enter the CSV in the document verification service of the Ministry of Justice's electronic office (sede electrónica del Ministerio de Justicia) and compare the result with the document you received.

If the format is invalid, checks[] contains csv_format with passed: false and the verdict becomes invalid with a csv_format reason of severity error.

The PDF's electronic signature

The certificate downloaded from the Ministry of Justice's e-government site is a PDF with an electronic signature (the Ministry's seal). Constaia verifies it at no extra cost and without contacting anyone: that the content hasn't been touched since it was signed and that the signer's certificate chains to a trusted authority. The result goes in signature and in verdict.reasons:

What arrivesReasonWithout require_valid_signatureWith require_valid_signature: true
The original PDF, intactsignature_validinfoinfo
A PDF modified after signingdocument_modified_after_signingwarning → reviewerror → invalid
A broken signaturesignature_invaliderror → invaliderror → invalid
An unsigned PDF, a photo or a scansignature_missingwarning → review for a PDF; not added for a photoerror → invalid

If you only accept the original PDF, add "require_valid_signature": true to checks: a photo or a "print to PDF" becomes invalid and the person knows they must upload the downloaded file. If you accept photos, leave it off and review the review cases by hand. Also compare signature.signer with the expected body: a valid signature only proves who signed. Details and limits in Digital signatures in PDF.

The code

check-lopivi.js
import { Constaia, ConstaiaError } from "@constaia/sdk";
import { fromPath } from "@constaia/sdk/node";

const constaia = new Constaia(); // reads CONSTAIA_API_KEY

export async function checkSexualOffencesCertificate(path, person) {
  const analysis = await constaia.analyze(await fromPath(path), {
    expect: "es_sexual_offences_certificate",
    checks: {
      maxAgeDays: 90,
      holder: { fullName: person.fullName, documentNumber: person.documentNumber },
    },
    storage: "none",
    keepResults: false,
    language: "en",
    metadata: { volunteer_id: String(person.id) },
  });

  return {
    analysisId: analysis.id,
    checkedAt: analysis.completed_at,
    status: analysis.verdict?.status ?? "review",
    problems: (analysis.verdict?.reasons ?? [])
      .filter((r) => r.severity !== "info")
      .map((r) => r.message),
  };
}

try {
  const result = await checkSexualOffencesCertificate("./sexual_offences_certificate.pdf", {
    id: 311,
    fullName: "María García López",
    documentNumber: "12345678Z",
  });
  console.log(result);
} catch (err) {
  if (err instanceof ConstaiaError) console.error(err.code, err.message, err.requestId);
  else throw err;
}

What it returns

With the test file sexual_offences_certificate.pdf and the options of the curl example (messages in Spanish, the default language), if the PDF is the signed original (otherwise you'll also see signature_missing with warning and a review verdict; see Test it):

verdict
{
  "expected": ["es_sexual_offences_certificate"],
  "match": true,
  "status": "valid",
  "reasons": [
    { "code": "type_match", "severity": "info", "message": "El documento es Certificado de delitos de naturaleza sexual." },
    { "code": "max_age_days", "severity": "info", "message": "Emitido hace 14 días (máximo 90)." },
    { "code": "holder", "severity": "info", "message": "Los datos del titular coinciden (full_name)." },
    { "code": "has_records", "severity": "info", "message": "El certificado indica que no constan antecedentes." }
  ]
}

And in checks[]: nif_check_digit (the holder's DNI check letter, 12345678Z) and csv_format with passed: true. The fields are holder_name (MARÍA GARCÍA LÓPEZ), holder_id (12345678Z), issue_date (2026-09-15), has_records (false) and csv_code (MJU4-7K2P-9QXA-3ZTR). The number of days depends on the date you run it.

What to store in your system

Since Constaia keeps nothing (keep_results: false), your record is the only trace. Store the minimum:

DataWhat for
analysis.idReference for incidents or support requests. You won't be able to retrieve the result with it.
Check date (completed_at)Know when it expires under your window and when to ask again.
Result (valid, invalid, review)The decision taken.

Don't keep the PDF or the other fields unless you need them. If you keep csv_code to verify it later at the electronic office, treat it as sensitive data: it can be used to locate the certificate.

Decide

VerdictAction
VálidoRecord the date and allow the person to work with minors.
No válidoIf the reason is has_records, follow your internal protocol. If it's age, holder or type, ask for a new certificate.
RevisarHuman review: poor photo, unreadable issue date or low confidence.

For review you need your own copy of the document, because with storage: "none" Constaia doesn't keep it. See Human review.

Test it

With a ck_test_… key, a PDF named sexual_offences_certificate.pdf (or any name containing sexual, delitos or penales) returns the no-records certificate of MARÍA GARCÍA LÓPEZ issued on 2026-09-15. Change holder.full_name to see invalid, or lower max_age_days to 7 to see the max_age_days reason with error. More in Test mode.

The signature is not simulated: with any unsigned PDF you'll get signature_missing (warning) and a review verdict. Use a JPEG or PNG image with that name to see the valid in the table, or your own certificate downloaded from the e-government site to see signature_valid.

Next steps

On this page