// API Documentation · REST · JSON · eIDAS

Qualified Electronic
Signatures as a Service

QSignHub is an API-first platform for qualified electronic signatures (QES) fully compliant with the eIDAS regulation. Sign legally binding documents across the EU in minutes — without building your own PKI infrastructure.

eIDASCOMPLIANT
QESSIGNATURE LEVEL
<2sRESPONSE TIME
99.9%UPTIME SLA
BASE URL https://api.qsignhub.io/v1 SANDBOX https://sandbox.qsignhub.io/v1
01

Overview

QSignHub lets you integrate qualified electronic signatures directly into your application via a clean REST API. Every signature request goes through a complete identity verification cycle, cryptographic signing using a qualified certificate issued by an EU-trusted CA, and timestamping via an accredited TSA.

ℹ️
Signatures generated by QSignHub carry the same legal weight as handwritten signatures across the entire European Union under eIDAS Regulation (Art. 25(2)). No further verification by counterparties is required.

Signature types

QES · Qualified
highest_level
Full legal validity. Video identity verification + qualified certificate. Required for notarial deeds, loan agreements, and public procurement.
AES · Advanced
advanced_level
Email + SMS OTP verification, non-qualified certificate. Sufficient for most B2B and B2C contracts.
SES · Simple
simple_level
Click-to-sign with IP logging and timestamp. Fastest option, used for T&C acceptance and basic acknowledgments.
PAdES / XAdES / CAdES
format
Supported signing formats. Default is PAdES for PDF, XAdES for XML, CAdES for all other file types.
02

Authentication

The API uses API keys in the X-API-Key header to authorize requests. Generate keys from the developer dashboard. Production keys have the prefix qsh_live_, test keys use qsh_test_.

HTTP HEADERS
# Required on every request
X-API-Key: qsh_live_Xk9mN3pQr7vT2wL8yE5uH1jA4cB6dF0g
X-Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000  # optional, recommended for POST
⚠️
Never store API keys in source code or version control. Use environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault).

Rate limiting

Rate limit headers are included in every response:

RESPONSE HEADERS
X-RateLimit-Limit: 1000          # requests per minute (Startup plan)
X-RateLimit-Remaining: 847       # remaining in current window
X-RateLimit-Reset: 1735689600    # Unix timestamp of window reset
X-RateLimit-Tier: "startup"       # active plan tier
03

Signing Flow

The standard qualified signature process consists of five steps:

POST /documents
1. Upload document
POST /signature-requests
2. Create request
Identity Check
3. Signer verifies
HSM + TSA
4. Sign & timestamp
Webhook + PDF
5. Delivery
Identity verification and signature issuance happen on QSignHub infrastructure. The signer is redirected to a Hosted Signing Page under your branding, or signs via an embeddable SDK within your application.
04

Signature Requests

POST /signature-requests Creates a new signature request
Parameters
Request example
Response 201
FieldTypeDescription
document_id requiredstringID of a document previously uploaded via POST /documents
signers requiredarray[Signer]List of signers. Min 1, max 20.
signature_type optionalenumqes | aes | ses. Defaults to qes.
signing_order optionalenumsequential | parallel. Defaults to parallel.
expires_at optionaldatetimeISO 8601. Request expiry deadline. Defaults to 30 days from creation.
redirect_url optionalstringURL to redirect the signer to after signing is complete.
fields optionalarray[Field]Signature field positions on the document (page, x, y, width, height).
metadata optionalobjectArbitrary key-value pairs. Returned in webhook payloads.
JSON — REQUEST BODY
{
  "document_id": "doc_9Xk3mN7pQr2vT8wL",
  "signature_type": "qes",
  "signing_order": "sequential",
  "expires_at": "2025-03-01T23:59:59Z",
  "redirect_url": "https://yourapp.com/contract/success",
  "signers": [
    {
      "name": "Anna Kowalska",
      "email": "anna.kowalska@company.com",
      "phone": "+48500123456",
      "identity_verification": {
        "method": "video_id",      // video_id | eidas_idp | bank_id
        "country": "PL"
      },
      "locale": "en-GB",
      "order": 1
    },
    {
      "name": "John Smith",
      "email": "john.smith@company.com",
      "order": 2
    }
  ],
  "fields": [
    {
      "signer_index": 0,
      "page": 3,
      "x": 72, "y": 640,
      "width": 200, "height": 80,
      "type": "signature"
    }
  ],
  "metadata": {
    "contract_id": "CNT-2025-0042",
    "department": "legal"
  }
}
JSON — RESPONSE 201 CREATED
{
  "id": "sreq_7Yx4kP9nQ3mB2wR1",
  "status": "pending",
  "signature_type": "qes",
  "document_id": "doc_9Xk3mN7pQr2vT8wL",
  "signing_url": "https://sign.qsignhub.io/r/7Yx4kP9nQ3mB2wR1",  // hosted signing page
  "embed_token": "eyJhbGciOiJSUzI1NiJ9...",                      // for SDK iframe embed
  "signers": [
    {
      "id": "sgn_A1b2C3d4E5f6",
      "name": "Anna Kowalska",
      "email": "anna.kowalska@company.com",
      "status": "pending_verification",
      "signing_url": "https://sign.qsignhub.io/s/A1b2C3d4E5f6"
    }
  ],
  "expires_at": "2025-03-01T23:59:59Z",
  "created_at": "2025-01-15T10:34:22Z"
}
GET /signature-requests/{request_id} Retrieves the status of a signature request

Returns the full state of a signature request, including per-signer statuses and certificate metadata.

JSON — RESPONSE 200 OK (after completion)
{
  "id": "sreq_7Yx4kP9nQ3mB2wR1",
  "status": "completed",
  "signature_type": "qes",
  "signed_document_url": "https://cdn.qsignhub.io/docs/sreq_7Yx4.../signed.pdf",
  "audit_trail_url": "https://cdn.qsignhub.io/audit/sreq_7Yx4....pdf",
  "signers": [
    {
      "id": "sgn_A1b2C3d4E5f6",
      "name": "Anna Kowalska",
      "status": "signed",
      "signed_at": "2025-01-15T11:02:44Z",
      "certificate": {
        "issuer": "Krajowa Izba Rozliczeniowa S.A.",
        "serial": "3A:F2:9C:11:00:BB:EE:44",
        "valid_from": "2024-06-01",
        "valid_to": "2026-06-01",
        "qualified": true,
        "tsp": "CERTUM Certification Centre"
      },
      "timestamp": {
        "value": "2025-01-15T11:02:45.321Z",
        "authority": "CERTUM TSA",
        "hash": "sha256:e3b0c44298fc1c149afb..."
      }
    }
  ],
  "completed_at": "2025-01-15T11:02:46Z"
}

Request statuses

● pending
Request created, awaiting signer action
● in_progress
At least one signer has signed (sequential mode)
● completed
All signers have signed the document
● expired / cancelled
Expired or cancelled by the initiator
GET /signature-requests Lists requests with filtering and cursor pagination
Query paramTypeDescription
statusstringFilter by status: pending, completed, expired, cancelled
fromdatetimeISO 8601 — start of date range
todatetimeISO 8601 — end of date range
limitintegerResults per page. Max 100, default 20.
cursorstringPagination cursor (from next_cursor in previous response)
DELETE /signature-requests/{request_id} Cancels a signature request

Cancellation is irreversible. Only requests with status pending or in_progress can be cancelled. Signers will receive an email notification.

REQUEST BODY (optional)
{ "reason": "Contract terms updated — a revised version will be sent shortly." }
05

Documents

POST /documents Uploads a document for signing

Accepts multipart/form-data. Supported formats: PDF (recommended), DOCX, XLSX, XML. Maximum file size: 50 MB. Documents are retained for 90 days from upload.

cURL
curl -X POST https://api.qsignhub.io/v1/documents   -H "X-API-Key: qsh_live_Xk9mN3pQr7..."   -F "file=@service_agreement.pdf"   -F "name=Q1 2025 Service Agreement"   -F "locale=en-GB"
RESPONSE 201
{
  "id": "doc_9Xk3mN7pQr2vT8wL",
  "name": "Q1 2025 Service Agreement",
  "mime_type": "application/pdf",
  "size_bytes": 248320,
  "pages": 4,
  "sha256": "a4b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5...",
  "preview_url": "https://cdn.qsignhub.io/preview/doc_9Xk3.../page-1.png",
  "created_at": "2025-01-15T10:30:00Z",
  "expires_at": "2025-04-15T10:30:00Z"
}
GET /signature-requests/{request_id}/download Downloads the signed document

Returns the signed PDF with embedded digital signatures (PAdES-LTV), a TSA timestamp, and an attached Audit Trail. Only available when request status is completed.

ℹ️
The ?include=audit_trail query parameter returns a ZIP archive containing: the signed PDF, an HTML action history report, identity verification screenshots, and all signer certificates.
06

Signers

POST /signature-requests/{request_id}/signers Adds a signer to an existing request

Only possible when the request is in pending status. The new signer will receive an email invitation link.

FieldTypeDescription
name requiredstringFull name of the signer
email requiredstringEmail address
phone optionalstringPhone number in E.164 format (required for SMS OTP)
identity_verification.methodenumvideo_id · eidas_idp · bank_id · sms_otp
identity_verification.countrystringISO 3166-1 alpha-2 country code
locale optionalstringBCP 47 locale (e.g. en-GB, de-DE, pl-PL)
GET /signature-requests/{request_id}/signers/{signer_id}/verification Identity verification status for a signer
RESPONSE 200
{
  "signer_id": "sgn_A1b2C3d4E5f6",
  "verification_status": "verified",  // pending | in_progress | verified | failed
  "method_used": "video_id",
  "verified_at": "2025-01-15T10:58:12Z",
  "identity": {
    "full_name": "ANNA MARIA KOWALSKA",    // from identity document
    "document_type": "national_id",           // passport | national_id | drivers_license
    "document_country": "PL",
    "date_of_birth": "1985-04-22",
    "document_expiry": "2030-11-15",
    "match_score": 0.97               // face match score 0.0-1.0
  },
  "liveness_check": true
}
POST /signature-requests/{request_id}/signers/{signer_id}/remind Sends an email / SMS reminder

Sends a reminder to a signer. Limit: 3 times per signer, no more than once every 24 hours.

REQUEST BODY (optional)
{
  "channel": "email",      // email | sms | both
  "message": "Please sign the document before the end of this week."
}
07

Certificates & Validation

POST /validate Validates a signed document (internal or external)

Verifies the integrity of an electronic signature on a PDF document. Works for documents signed via QSignHub as well as any other eIDAS-compliant system (PAdES, XAdES, CAdES).

cURL
curl -X POST https://api.qsignhub.io/v1/validate   -H "X-API-Key: qsh_live_..."   -F "file=@signed_agreement.pdf"
RESPONSE 200
{
  "valid": true,
  "signatures": [
    {
      "signer_name": "Anna Maria Kowalska",
      "signed_at": "2025-01-15T11:02:45.321Z",
      "signature_level": "QES",
      "certificate_valid": true,
      "certificate_issuer": "Krajowa Izba Rozliczeniowa S.A.",
      "trusted_list": "EU_TRUSTED",      // present on EU TSL
      "document_integrity": true,     // document unaltered since signing
      "timestamp_valid": true,
      "ocsp_status": "good"            // good | revoked | unknown
    }
  ]
}
08

Webhooks

Configure an HTTPS endpoint in the dashboard to receive real-time events. QSignHub authenticates events using an HMAC-SHA256 signature in the X-QSignHub-Signature header.

Events

signature_request.createdA new signature request was initiated
signer.verification_completedSigner identity verification finished
signer.signedA signer successfully signed the document
signature_request.completedAll signers signed — document ready for download
signature_request.expiredRequest expired before all signatures were collected
signer.verification_failedIdentity verification failed (poor quality, refusal)
EXAMPLE WEBHOOK PAYLOAD
// POST https://yourapp.com/webhooks/qsignhub
// X-QSignHub-Signature: sha256=a4f2b1c9d8e7f6a5...

{
  "id": "evt_Z9wX8vU7tS6rQ5pO",
  "type": "signature_request.completed",
  "created_at": "2025-01-15T11:02:50Z",
  "data": {
    "signature_request": {
      "id": "sreq_7Yx4kP9nQ3mB2wR1",
      "status": "completed",
      "signed_document_url": "https://cdn.qsignhub.io/docs/.../signed.pdf",
      "metadata": { "contract_id": "CNT-2025-0042" }
    }
  }
}

HMAC Signature Verification

NODE.JS
const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');

  const received = signature.replace('sha256=', '');
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(received, 'hex')
  );
}
09

Error Codes

200
OK — request succeeded
201
Created — resource created
400
Bad Request — malformed parameters
401
Unauthorized — missing or invalid API key
409
Conflict — duplicate Idempotency-Key
422
Unprocessable — validation failed
429
Rate Limited — too many requests
500
Internal Error — contact support
ERROR BODY STRUCTURE
{
  "error": {
    "code": "DOCUMENT_TOO_LARGE",
    "message": "File exceeds the 50 MB limit. Received: 62.3 MB.",
    "docs_url": "https://docs.qsignhub.io/errors/DOCUMENT_TOO_LARGE",
    "request_id": "req_V1W2X3Y4Z5A6B7C8"  // for support reference
  }
}
10

Sandbox Environment

The sandbox is fully isolated from production. Test keys use the prefix qsh_test_. No real signatures are generated — all identity checks and certificates are mocked.

🧪
To simulate specific scenarios in sandbox, use magic email addresses: sign.success@qsignhub-test.io (auto-success), sign.fail@qsignhub-test.io (failed verification), sign.timeout@qsignhub-test.io (no signer action).

Test identity document numbers

AZE123456
national_id · PL
Simulates successful verification, match_score: 0.98
FAIL000001
passport · ANY
Simulates verification failure — document expired
REVOKED001
national_id · DE
Simulates revoked certificate (OCSP: revoked)
LOWSCORE01
national_id · PL
Low face match score: 0.61 — verification fails
11

Identity Providers (eIDAS IdP)

When using the eidas_idp method, the signer is redirected to their national identity provider. Supported providers and countries:

🇵🇱 Poland
Profil Zaufany · mObywatel · KIR
Full QES support. Qualified certificates from KIR, CERTUM, SZAFIR.
🇩🇪 Germany
BankID · Bundesdruckerei
QES via nPA (new national ID card) and banking services.
🇫🇷 France
France Identité · La Poste
QES and AES via France Connect+.
🇪🇸 🇮🇹 🇨🇿 +14
and other EU countries
Full list of supported countries: GET /v1/idp/countries