Skip to content

Verification

Anyone can check the signatures of a signed PDF: a TrustInk document, or any other. Adobe Reader shows TrustInk’s signatures as “validity unknown” until the TrustInk root is trusted; the verify routes validate them, and every other PDF signature, with the EU’s DSS library. The page trustink.io/verify uses the same routes.

Two public routes, without authorization:

Route What it does
POST /verify/upload answers a presigned PUT for the PDF to check
POST /verify validates the PDF uploaded under key and answers the report

The PDF goes straight to storage, so documents up to 25 MB fit.

  1. Ask for an upload URL. The request takes no body:

    Terminal window
    API=https://api.trustink.io
    UPLOAD=$(curl -s -X POST $API/verify/upload)
    {
    "key": "verify/7c1f4d7e-9b1a-4c55-8f0e-2d6a1b3c4e5f.pdf",
    "url": "https://…",
    "method": "PUT",
    "headers": {"Content-Type": "application/pdf"},
    "expiresAt": "2026-09-26T12:15:00.000Z",
    "maxSize": 26214400
    }

    The URL works for 15 minutes.

  2. PUT the PDF:

    Terminal window
    curl -s -X PUT -H 'Content-Type: application/pdf' --upload-file signed.pdf "$(echo "$UPLOAD" | jq -r .url)"
  3. Validate it:

    Terminal window
    curl -s -X POST -H 'Content-Type: application/json' \
    -d "{\"key\":\"$(echo "$UPLOAD" | jq -r .key)\"}" $API/verify

    The upload is deleted once it is validated.

{
"documentSha256": "c54348f52e84885bc0666df13168cf14fe842d178fc649891cb9d63ae6054eb2",
"issuedByTrustInk": true,
"validationTime": "2026-09-26T15:23:48.077Z",
"trustedListsIssuedAt": "2026-09-24T12:04:06Z",
"signatures": [
{
"id": "S-3F5C04CAC7B2CABDFB700EB35661335D5FD34908CBC900B89FC06B84AD15BB52",
"signedBy": "Jana Nováková",
"signingTime": "2026-09-26T11:48:19Z",
"bestSignatureTime": "2026-09-26T15:23:48.077Z",
"indication": "TOTAL_PASSED",
"level": "LTA",
"format": "PAdES-BASELINE-LTA",
"qualification": "N/A",
"issuedByTrustInk": true,
"timestamps": [
{"type": "SIGNATURE_TIMESTAMP", "producedBy": "www.freetsa.org", "productionTime": "2026-09-26T11:48:21Z",
"indication": "INDETERMINATE", "subIndication": "NO_CERTIFICATE_CHAIN_FOUND", "qualification": "N/A"},
{"type": "DOCUMENT_TIMESTAMP", "producedBy": "www.freetsa.org", "productionTime": "2026-09-26T11:48:35Z",
"indication": "INDETERMINATE", "subIndication": "NO_CERTIFICATE_CHAIN_FOUND", "qualification": "N/A"}
]
}
]
}

This is a test document. Its timestamps come from a timestamp authority that no trusted list names, which is why they are INDETERMINATE and bestSignatureTime is the validation time. TrustInk now asks a qualified timestamp authority first, whose timestamps count as QTSA.

Field Meaning
documentSha256 hex SHA-256 of the uploaded file
issuedByTrustInk a signature’s certificate chains to the TrustInk root; also per signature
trustedListsIssuedAt when the EU list of trusted lists the validation used was issued
signatures[].indication ETSI EN 319 102-1: TOTAL_PASSED, INDETERMINATE or TOTAL_FAILED; subIndication says why, e.g. HASH_FAILURE for a document changed after signing, NO_CERTIFICATE_CHAIN_FOUND for a signer nobody trusts, OUT_OF_BOUNDS_NO_POE for an expired certificate without a trusted timestamp
signatures[].level B-B, B-T, LT or LTA; format has DSS’s full name (PAdES-BASELINE-LTA, PKCS7-B, PDF-NOT-ETSI)
signatures[].qualification the eIDAS qualification as DSS names it: QESig, QESeal, AdESig-QC, AdESig, N/A, or Indeterminate QESig and the like when the signature could not be proven
signatures[].bestSignatureTime the earliest time the signature is proven to exist: its first trusted timestamp, else the validation time
signatures[].timestamps the signature’s own timestamp and the document timestamps that cover it, each with its indication and QTSA, TSA or N/A
signatures[].errors DSS’s reasons for an indication other than TOTAL_PASSED

An unsigned PDF is not an error: it answers signatures: [].

Validation uses DSS with its default validation policy and trusts two sources:

  • The TrustInk root, so TrustInk’s documents pass although no trusted list names the TrustInk PKI.
  • The EU trusted lists: the EU list of trusted lists and the national lists it points to, refreshed daily, for everyone else’s signatures and for the timestamp authorities. They decide the qualification.

Revocation data a document does not carry is fetched online. TrustInk’s documents carry theirs: they are extended to PAdES B-LTA, so they stay verifiable after the signers’ one-shot certificates, valid 24 hours, have expired.

Status code When
400 (validationErrors) no key, other fields, or a key that is not verify/<uuid>.pdf
404 DOCUMENT_NOT_FOUND nothing was uploaded under the key, the upload expired, or it was verified already
413 DOCUMENT_TOO_LARGE the upload is larger than 25 MB; it is deleted
422 NOT_A_PDF the upload does not start with %PDF- in its first kilobyte; it is deleted
422 UNREADABLE_DOCUMENT the PDF or its signatures cannot be read; it is deleted
429 too many requests ({message} only)
503 TIMEOUT the validation did not finish in time; the upload stays for a retry

Both routes are rate limited for all callers together: POST /verify/upload at 2 requests a second (burst 10), POST /verify at 1 a second (burst 5). Retry a 429 after a short pause.

Every case keeps an audit trail: who did what, when, from where, and on which document. It is the evidence behind the signature. Read it with GET /cases/{caseId}/events, newest first:

{
"events": [
{"eventId": "…", "type": "participant.signed", "occurredAt": "2026-09-24T08:00:01.512Z", "actor": "participant",
"participantId": "…", "ip": "203.0.113.7", "userAgent": "Mozilla/5.0 …", "documentSha256": "9f2c…",
"data": {"signatureCount": 1}}
],
"nextCursor": "eyJwayI6…"
}

?limit= takes 1 to 100 events per page (100 by default); anything else answers 400 INVALID_LIMIT. While older events remain, nextCursor is set: pass it as ?cursor= for the next page. A cursor continues only the listing it came from; any other answers 400 INVALID_CURSOR.

The trail records, among others, case.created, document.uploaded (with the document’s SHA-256), participant.invited, participant.viewed, participant.otp_verified, participant.consented, participant.signed, participant.certificate_issued, participant.signature_applied, case.finalised and case.completed. Events caused by a request carry the IP address and user agent. participant.viewed, participant.consented and participant.signed carry the SHA-256 of the original, so the trail shows which bytes the participant saw and agreed to. The trail never holds tokens or the participants’ personal data.

POST /participant/{participantId}/signature requires the consent the signing page showed: consent: {textVersion, acceptedAt}, with acceptedAt an ISO timestamp with an offset. Optional client hints (time zone, language, screen size, whether every page was viewed) are recorded with it.

Next to the signed PDF, every completed case has a sealed evidence summary PDF (evidenceDocument), extended to PAdES B-LTA like the document. It names the case, the original’s SHA-256 and the signed document’s name, SHA-256 and level; for each participant the masked contact, when they were invited, opened the document, verified the one-time code, consented, signed and had the signature applied, the IP address and browser, the consent text version and their certificate; and the seal and timestamp authorities. It is a separate PDF on purpose: a page added to the signed document would break its signatures.