Download the latest Windows installer from Releases. Prefer NexoSign_*_x64-setup.exe (per-user, no admin required). The .msi is for system-wide / IT installs and typically asks for administrator rights.
- macOS bundles from CI are not notarized; see
docs/distribucion-macos.md. - Development setup: CONTRIBUTING.md.
| Document | Purpose |
|---|---|
| LICENSE | MIT |
| SECURITY.md | Vulnerability reporting |
| CODE_OF_CONDUCT.md | Community standards |
| CHANGELOG.md | Release history |
Internal planning notes: docs/internal/PLAN.md.
| 🔒 Privacy by design | Signing and the PIN happen on the user’s machine. The API listens on loopback only — not a SaaS that stores your PDFs or keys. |
| 🖲️ Real hardware | PKCS#11: same model as smart cards, Spanish DNIe, and HSMs. One queue, one signer: parallelism must not break what the chip cannot do. |
| 🧩 Your web, the desktop | From the browser you can register an intent (POST …/intent), then POST /api/v1/focus with { "intent": "<request_id>" } so the user completes the wizard (certificate, PIN, stamp cell). |
| ⚙️ Local automation | The desktop app and tools on the same machine may use internal HTTP routes not in OpenAPI; remote integrators use intent → status → downloads. |
- Source — Individual PDFs or an entire folder (all
.pdffiles, including subfolders). Folder signing writes output toFolderName_signednext to the chosen folder. - Certificate — Pick from signing certificates discovered via PKCS#11 (and Windows MY on Windows).
- PIN — Only to unlock the token for that operation; no long-lived PKCS#11 session.
- Placement and confirm — Grid on the first page, local queue, and batch progress.
📁 Output: {name}_signed.pdf next to the original, or inside …_signed when signing by folder.
Background: closing the window does not quit the app (local API stays active). Restore the window from the system tray (Open NexoSign). To exit completely, use Quit in the tray or the application menu (e.g. Cmd+Q on macOS).
The API listens at http://127.0.0.1:14500 only while the app is running (npm run tauri dev or an installed build).
Desktop app: the UI uses Tauri invoke (local_api_* Rust commands) for health, ping, enqueue signing, and job status — same logic as the HTTP endpoints, without fetch to loopback (avoids CORS and mixed-content issues in release). 127.0.0.1:14500 remains the channel for integrators, portals, and external tools.
Single instance: only one NexoSign process runs at a time; launching again focuses the existing window.
Port 14500 busy: if another program holds the port, the HTTP API does not start (fail-fast). The desktop app still signs via IPC; check Settings → Servicio local or logs. Override the port with NEXOSIGN_LOCAL_API_PORT (single port, no auto-range). GET /health includes port and baseUrl when listening.
| Requirement | Details |
|---|---|
| 🌍 Origin | Browser POST and GET for batch routes need an Origin header allowed by CORS (e.g. http://localhost:1420). Includes intent status polling and downloads. |
💻 curl |
Add -H "Origin: http://localhost:1420" as in the examples. |
| 📘 OpenAPI | With the app running: GET /openapi.json — intent, intent status, batch downloads, GET /health, POST /api/v1/ping. GET /docs serves Swagger UI. Import into Scalar, Postman, etc. |
| 📂 Multipart vs signing | multipart/form-data only on POST …/batch/sign/intent (upload PDFs from the browser). Signing runs in the app after POST /api/v1/focus; direct POST …/batch/sign is not in openapi.json (web integrator contract). |
| Endpoint | Role |
|---|---|
GET /api/v1/batch/jobs/{job_id}/signed-files |
When the batch has finished: JSON with files[] (index, filename, href) to download each signed PDF from the browser (same Origin as POST). |
GET /api/v1/batch/jobs/{job_id}/files/{i} |
application/pdf for signed file i (same order as inputs / successful items only). |
GET /api/v1/batch/sign/intent/{request_id}/status |
Poll after intent: phase = awaiting_confirmation | processing | completed, job_id, manifest_href, signed_file_count. No backend required: your page polls 127.0.0.1:14500 with the same Origin. |
POST /api/v1/batch/sign/intent |
Does not sign yet. application/json (inputs: absolute paths) or multipart/form-data with repeatable files (one PDF per part). Uploads go to temporary staging; returns request_id. Expires if not confirmed in time (same window as max batch job time; default ~5 min, set via NEXOSIGN_BATCH_JOB_MAX_SECS). |
POST /api/v1/focus |
Brings NexoSign to the front; optional body { "intent": "<request_id>" } opens the signing wizard for that intent. |
GET /health |
Service status (no Origin). |
POST /api/v1/ping |
Echo for smoke tests. |
NEXOSIGN_BATCH_OUTPUT_DIR |
Env var: force global output folder {stem}_signed.pdf. |
NEXOSIGN_BATCH_JOB_MAX_SECS |
Max window (seconds) for pending intents and queued batch jobs (default 300). Large batches or load tests: increase (e.g. 7200). |
Use this when the user must pick certificate and PIN in the app (not via a hidden POST from your server).
flowchart LR
subgraph Web["Your integration"]
A[Web page or local agent]
end
subgraph NexoSign["User machine"]
B["API 127.0.0.1:14500"]
D["NexoSign app — wizard"]
end
A -->|"POST /api/v1/batch/sign/intent"| B
B -->|"request_id"| A
A -->|"POST /api/v1/focus intent"| B
B --> D
D -->|"Confirm and enqueue (internal)"| B
Steps
- JSON: PDFs are already on disk (absolute paths) on that PC.
- Multipart: the browser sends PDFs in repeatable
files(up to 20 PDFs, 50 MiB each, total cap 20×50 MiB); the API stages temporaries and the wizard treats them as batch inputs. - Your client calls
POST /api/v1/batch/sign/intent(JSON or multipart). - You receive
request_id— callPOST /api/v1/focuswith{ "intent": "<request_id>" }(e.g. “Open in NexoSign” button). - The user completes the wizard; on confirm, NexoSign enqueues signing locally and links the intent
request_idto the job (job_id) for polling and downloads; staging is removed when the batch finishes.
No HTTP callback to your server is required. With the intent request_id:
- Call
POST /api/v1/focuswith{ "intent": "<request_id>" }to launch the wizard. - From the same tab (origin already allowed in Settings), poll every few seconds:
GET http://127.0.0.1:14500/api/v1/batch/sign/intent/{request_id}/status
withOrigin(your portal’s origin). - When
phaseiscompleted, usemanifest_href(orjob_id) for
GET …/batch/jobs/{job_id}/signed-files, then eachGET …/files/{i}, and save blobs in the client.
Until then you will see awaiting_confirmation (intent open) or processing (job queued, signing in progress).
Note: without an intermediate backend, polling is always to loopback from the user’s browser (same machine as NexoSign).
Product note: portals previously could not learn
job_id; the status endpoint completes that flow.
GET /openapi.jsoncovers intent, intent status, batch downloads,GET /health, andPOST /api/v1/ping(integrator contract). Routes likePOST /api/v1/batch/sign(direct enqueue with PIN) are for same-machine app use and do not appear in that JSON.
📋 Example — intent uploading PDF (multipart)
curl -sS -X POST "http://127.0.0.1:14500/api/v1/batch/sign/intent" \
-H "Origin: http://localhost:1420" \
-F "files=@/Users/tu/usuario/documentos/doc.pdf;type=application/pdf"📋 Example — intent with local paths (JSON)
curl -sS -X POST "http://127.0.0.1:14500/api/v1/batch/sign/intent" \
-H "Content-Type: application/json" \
-H "Origin: http://localhost:1420" \
-d "{\"inputs\": [\"/Users/tu/usuario/documentos/doc.pdf\"]}"{
"request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"
}Luego abre el asistente:
curl -sS -X POST "http://127.0.0.1:14500/api/v1/focus" \
-H "Content-Type: application/json" \
-H "Origin: http://localhost:1420" \
-d '{"intent":"f47ac10b-58cc-4372-a567-0e02b2c3d479"}'The desktop app and other local processes may use additional loopback HTTP routes; they are not in openapi.json. For integration from your domain, stick to intent → status polling → downloads.
| 📜 PAdES-BES | Detached CMS + RSA on token. |
| 🛡️ CORS | Allowed origins aligned with in-app policy / SQLite. |
📊 progreso |
IPC events per document for progress bars and logs. |
| 🔑 PKCS#11 | Module discovery, signing certificates, bounded session. |
| 🔐 PIN | Batch via loopback or pkcs11_login / pkcs11_logout commands depending on flow. |
| Variable | Purpose |
|---|---|
NEXOSIGN_PKCS11_MODULE |
Absolute path to .dll / .so / .dylib (overrides default paths). |
NEXOSIGN_PKCS11_SLOT |
Slot index (default 0). |
NEXOSIGN_BATCH_JOB_MAX_SECS |
Max time (seconds) for pending intents and batch queue in SQLite (default 300). |
If DNIe works in the system browser but NexoSign shows 0 slots, you often need different PKCS#11 middleware: try the official DNIe provider (FNMT/CCN) with NEXOSIGN_PKCS11_MODULE. OpenSC sometimes does not expose the card even when USB is recognized.
On Windows, besides PKCS#11, NexoSign lists signing certificates from the Personal (MY) store with RSA CNG keys (merged into the same list as the token). IDs start with winmy: + SHA-1 thumbprint. The UI indicates whether a PIN is required in-app or signing delegates to Windows Credential Manager (see docs/certificados-pkcs11-y-windows.md).
A .pfx on disk only or imported to the store without RSA CNG may not appear or may not sign in this flow. PKCS#11 middleware or hardware with its .dll remains the supported path.
Guides: docs/distribucion-windows.md and docs/distribucion-macos.md.
See scripts/load-test/README.md and scripts/gen-load-test-pdfs.mjs.
- Node.js (LTS)
- Rust and Tauri prerequisites
npm install
npm run tauri dev| Servicio | URL |
|---|---|
| Frontend | http://localhost:1420 |
| API | http://127.0.0.1:14500 |
export NEXOSIGN_ALLOWED_ORIGINS="https://my-app.example,http://localhost:3000"
npm run tauri devDefaults: localhost / 127.0.0.1 on ports 1420 (Tauri+Vite) and 5173.
| Layer | Command | Validates |
|---|---|---|
| Rust domain | cargo test -p nexosign --lib domain |
Origin policy |
| HTTP | cargo test -p nexosign --lib adapters::http |
Batch, intent, CORS |
| Contract | cargo test -p nexosign --test http_contract |
Router without OS process |
| TS client | npm run test |
Vitest |
| E2E UI | npm run test:e2e |
Playwright |
| E2E API | Terminal A: npm run tauri dev · B: NEXOSIGN_E2E_API=1 npm run test:e2e |
Contract against live API |
Without a server on :14500, network E2E tests are skipped (not failed).
First time: npx playwright install chromium.
npm run test
npm run test:e2e
cargo test --manifest-path src-tauri/Cargo.tomlSee CONTRIBUTING.md for setup, conventions, and pull requests. Architecture details: AGENTS.md.
VS Code · Svelte · Tauri · rust-analyzer