Per sviluppatori
Documentazione delle API
Con le API REST di SignOTP il tuo gestionale fa firmare un PDF a un cliente con un codice OTP via SMS: crei la pratica, carichi il documento, invii al cliente il link di firma e ricevi una notifica quando ha firmato.
https://api.signotp.comJSON su HTTPSheader x-api-keyAWS Europa (Milano)Avvio rapido
Il flusso completo in poche righe: crea la pratica, carica il PDF e ottieni il link da inviare al cliente. I client JavaScript e C# sono pronti da scaricare e non hanno dipendenze.
// Node.js 18+ — client: https://signotp.com/sviluppatori/signotp-client.mjs
import fs from 'node:fs/promises';
import { SignOtpClient } from './signotp-client.mjs';
const signotp = new SignOtpClient({ apiKey: process.env.SIGNOTP_API_KEY });
const { guid, signUrl } = await signotp.startSigning({
text: 'Gentile cliente,\nla preghiamo di firmare il contratto allegato.',
email: 'contratti@azienda.it',
callbackUrl: 'https://erp.azienda.it/signotp/callback', // facoltativo
pdfBytes: await fs.readFile('contratto.pdf'),
});
console.log(guid); // salvalo: identifica la pratica
console.log(signUrl); // invialo al cliente (es. via email)// .NET 6+ — client: https://signotp.com/sviluppatori/SignOtpClient.cs
using SignOtp;
using var signotp = new SignOtpClient(Environment.GetEnvironmentVariable("SIGNOTP_API_KEY")!);
var result = await signotp.StartSigningAsync(
new CreateRequest(
Text: "Gentile cliente,\nla preghiamo di firmare il contratto allegato.",
Email: "contratti@azienda.it",
CallbackUrl: "https://erp.azienda.it/signotp/callback"), // facoltativo
await File.ReadAllBytesAsync("contratto.pdf"));
Console.WriteLine(result.Guid); // salvalo: identifica la pratica
Console.WriteLine(result.SignUrl); // invialo al cliente (es. via email)# 1) crea la pratica
curl -X POST https://api.signotp.com/v1/requests \
-H "x-api-key: $SIGNOTP_API_KEY" -H "Content-Type: application/json" \
-d '{"text":"Gentile cliente,\nla preghiamo di firmare.","email":"contratti@azienda.it"}'
# 2) carica il PDF sulla upload.url restituita (senza x-api-key)
curl -X PUT "<upload.url>" -H "Content-Type: application/pdf" --data-binary @contratto.pdf
# 3) finalizza: la risposta contiene signUrl
curl -X POST https://api.signotp.com/v1/requests/<guid>/finalize -H "x-api-key: $SIGNOTP_API_KEY"Autenticazione
Ogni applicazione riceve da SignOTP una propria API key, da inviare in ogni chiamata nell'header x-api-key:
POST /v1/requests HTTP/1.1
Host: api.signotp.com
x-api-key: sotp_3c41a1e9012e_Xy7Qm…
Content-Type: application/json
- Ogni applicazione vede solo le proprie pratiche: quelle di altre applicazioni rispondono
404. - Insieme alla chiave ricevi un webhook secret, che serve a verificare le callback.
- Usa la chiave solo lato server: non inserirla mai in pagine web o in app installate sui dispositivi.
- Se la chiave viene esposta, chiedine la sostituzione: la vecchia smette di funzionare entro un minuto.
Per ottenere una chiave scrivi a info@signotp.com.
Flusso di firma
- Crei la pratica con
POST /v1/requestse ricevi unguide una URL di upload. - Carichi il PDF con un
PUTdiretto sulla URL di upload. - Finalizzi con
POST /v1/requests/{guid}/finalizee ricevi ilsignUrl. - Invii il signUrl al cliente, per esempio via email: SignOTP non invia email.
- Il cliente apre il link, legge il PDF, inserisce il cellulare e conferma con il codice OTP ricevuto via SMS.
- SignOTP crea il PDF firmato e chiama la tua callback; in alternativa puoi interrogare lo stato della pratica.
Il PDF firmato riporta su ogni pagina numero di telefono, data e ora, indirizzo IP e l'impronta SHA-256 del documento originale. Il codice OTP è generato e verificato da AWS: SignOTP non lo conosce e non lo conserva.
Crea una pratica
/v1/requestsAvvia una procedura di firma e restituisce la URL su cui caricare il PDF. Con existingUid aggiunge un nuovo documento a una pratica esistente (guid_02.pdf, guid_03.pdf…): se il documento precedente non era ancora firmato passa in stato SUPERSEDED.
| Campo | Tipo | Descrizione |
|---|---|---|
text | string | Messaggio del mittente mostrato nella pagina di firma (\n per andare a capo), max 5000 caratteri. Obbligatorio per una nuova pratica. |
email | string | Email del mittente, mostrata al cliente. Obbligatoria per una nuova pratica. |
callbackUrl | string | Facoltativo. URL https:// chiamata quando il documento viene firmato. |
existingUid | string | Facoltativo. guid di una tua pratica a cui aggiungere un nuovo documento; text, email e callbackUrl diventano facoltativi e, se passati, aggiornano la pratica. |
I nomi dei campi non distinguono maiuscole e minuscole. Risposta 201 Created.
const created = await signotp.createRequest({
text: 'Gentile cliente,\nla preghiamo di firmare il documento.',
email: 'contratti@azienda.it',
callbackUrl: 'https://erp.azienda.it/signotp/callback',
// existingUid: 'Xk3P9aQ2mZr7Lw0Bv5Nt8c', // nuovo documento sulla stessa pratica
});var created = await signotp.CreateRequestAsync(new CreateRequest(
Text: "Gentile cliente,\nla preghiamo di firmare il documento.",
Email: "contratti@azienda.it",
CallbackUrl: "https://erp.azienda.it/signotp/callback"
// , ExistingUid: "Xk3P9aQ2mZr7Lw0Bv5Nt8c" // nuovo documento sulla stessa pratica
));{
"guid": "Xk3P9aQ2mZr7Lw0Bv5Nt8c",
"docNumber": 1,
"fileName": "Xk3P9aQ2mZr7Lw0Bv5Nt8c_01.pdf",
"status": "UPLOAD_PENDING",
"upload": {
"method": "PUT",
"url": "https://signotp-documents.s3.eu-south-1.amazonaws.com/Xk3P…_01.pdf?X-Amz-…",
"headers": { "Content-Type": "application/pdf" },
"expiresAt": "2026-11-13T18:15:00.000Z",
"maxBytes": 20971520
},
"finalizeUrl": "https://api.signotp.com/v1/requests/Xk3P9aQ2mZr7Lw0Bv5Nt8c/finalize"
}Carica il PDF
{upload.url}Invia il contenuto binario del PDF alla URL restituita in upload.url, con l'header Content-Type: application/pdf e senza x-api-key. La URL vale 15 minuti; dimensione massima 20 MB.
await signotp.uploadPdf(created.upload, await fs.readFile('contratto.pdf'));await signotp.UploadPdfAsync(created.Upload, await File.ReadAllBytesAsync("contratto.pdf"));curl -X PUT "<upload.url>" -H "Content-Type: application/pdf" --data-binary @contratto.pdfFinalizza
/v1/requests/{guid}/finalizeVerifica che il file sia un PDF leggibile (non protetto da password), ne calcola lo SHA-256 e lo rende firmabile. Restituisce il signUrl da inviare al cliente. È idempotente: si può ripetere senza effetti collaterali.
const doc = await signotp.finalize(created.guid);
// doc.signUrl -> https://sms.signotp.com/Xk3P9aQ2mZr7Lw0Bv5Nt8cvar doc = await signotp.FinalizeAsync(created.Guid);
// doc.SignUrl -> https://sms.signotp.com/Xk3P9aQ2mZr7Lw0Bv5Nt8c{
"guid": "Xk3P9aQ2mZr7Lw0Bv5Nt8c",
"docNumber": 1,
"fileName": "Xk3P9aQ2mZr7Lw0Bv5Nt8c_01.pdf",
"status": "READY",
"pages": 3,
"sha256": "9f2c…e41a",
"signUrl": "https://sms.signotp.com/Xk3P9aQ2mZr7Lw0Bv5Nt8c"
}Stato della pratica
/v1/requests/{guid}Restituisce il mittente e tutti i documenti della pratica con il loro stato. Per i documenti firmati riporta telefono, IP, browser, data, impronte SHA-256 ed esito della callback. Utile come alternativa, o in aggiunta, alla callback.
const pratica = await signotp.getRequest(guid);
const doc = pratica.documents.at(-1);
if (doc.status === 'SIGNED') console.log('Firmato il', doc.signed.signedAt, 'da', doc.signed.phone);var pratica = await signotp.GetRequestAsync(guid);
var docs = pratica.GetProperty("documents");
var doc = docs[docs.GetArrayLength() - 1];
if (doc.GetProperty("status").GetString() == "SIGNED")
Console.WriteLine($"Firmato il {doc.GetProperty("signed").GetProperty("signedAt")}");{
"guid": "Xk3P9aQ2mZr7Lw0Bv5Nt8c",
"createdAt": "2026-11-13T17:00:00.000Z",
"updatedAt": "2026-11-13T17:20:31.412Z",
"sender": { "text": "Gentile cliente,\n…", "email": "contratti@azienda.it" },
"callbackUrl": "https://erp.azienda.it/signotp/callback",
"currentDoc": 1,
"signUrl": "https://sms.signotp.com/Xk3P9aQ2mZr7Lw0Bv5Nt8c",
"documents": [
{
"docNumber": 1,
"fileName": "Xk3P9aQ2mZr7Lw0Bv5Nt8c_01.pdf",
"status": "SIGNED",
"pages": 3,
"sha256": "9f2c…e41a",
"originalUrl": "https://api.signotp.com/v1/requests/Xk3P…/documents/01/original",
"signed": {
"fileName": "Xk3P9aQ2mZr7Lw0Bv5Nt8c_01_signed.pdf",
"signedAt": "2026-11-13T17:20:31.412Z",
"phone": "+393331234567",
"ip": "93.41.12.7",
"userAgent": "Mozilla/5.0 …",
"sha256": "1b7d…08aa",
"downloadUrl": "https://api.signotp.com/v1/requests/Xk3P…/documents/01/signed"
},
"callback": { "ok": true, "httpStatus": 200, "attempts": 1 }
}
]
}Scarica i PDF
/v1/requests/{guid}/documents/{n}/signedRisponde 302 verso una URL temporanea (5 minuti) del PDF firmato; i client HTTP seguono il redirect in automatico. Con /original al posto di /signed si scarica il PDF originale. n è il numero del documento (1 oppure 01).
const pdf = await signotp.download(guid, 1); // PDF firmato
// const original = await signotp.download(guid, 1, 'original');
await fs.writeFile(`${guid}_01_signed.pdf`, pdf);var pdf = await signotp.DownloadAsync(guid, 1); // PDF firmato
// var original = await signotp.DownloadAsync(guid, 1, signed: false);
await File.WriteAllBytesAsync($"{guid}_01_signed.pdf", pdf);curl -L -o firmato.pdf https://api.signotp.com/v1/requests/<guid>/documents/01/signed \
-H "x-api-key: $SIGNOTP_API_KEY"Callback
Se hai indicato una callbackUrl, quando il cliente firma SignOTP invia una POST con questo corpo JSON:
{
"event": "document.signed",
"guid": "Xk3P9aQ2mZr7Lw0Bv5Nt8c",
"docNumber": 1,
"fileName": "Xk3P9aQ2mZr7Lw0Bv5Nt8c_01_signed.pdf",
"signedAt": "2026-11-13T17:20:31.412Z",
"phone": "+393331234567",
"ip": "93.41.12.7",
"sha256": { "original": "9f2c…e41a", "signed": "1b7d…08aa" },
"signedPdfUrl": "https://signotp-documents.s3.eu-south-1.amazonaws.com/…",
"signedPdfUrlExpiresAt": "2026-11-13T18:20:31.000Z",
"downloadUrl": "https://api.signotp.com/v1/requests/Xk3P9aQ2mZr7Lw0Bv5Nt8c/documents/01/signed"
}
Ogni callback è firmata con il webhook secret della tua applicazione:
| Header | Valore |
|---|---|
X-SignOTP-Event | document.signed |
X-SignOTP-Timestamp | istante dell'invio, in secondi Unix |
X-SignOTP-Signature | sha256= seguito dall'HMAC-SHA256 esadecimale di timestamp + "." + corpo, con chiave il webhook secret |
- Verifica la firma sul corpo grezzo, prima di interpretarlo come JSON, e scarta le callback con timestamp più vecchio di 5 minuti.
- Rispondi con un codice
2xxentro 5 secondi; altrimenti SignOTP ritenta una sola volta. L'esito resta visibile indocuments[].callback. signedPdfUrlsi scarica senza autenticazione per circa un'ora;downloadUrlè permanente e richiedex-api-key.
// Express: serve il corpo "grezzo" per verificare la firma
import express from 'express';
import { verifyCallback } from './signotp-client.mjs';
const app = express();
app.post('/signotp/callback', express.raw({ type: 'application/json' }), async (req, res) => {
const rawBody = req.body.toString('utf8');
const valid = verifyCallback({
rawBody,
timestamp: req.get('X-SignOTP-Timestamp'),
signature: req.get('X-SignOTP-Signature'),
secret: process.env.SIGNOTP_WEBHOOK_SECRET,
});
if (!valid) return res.sendStatus(401);
const evt = JSON.parse(rawBody);
if (evt.event === 'document.signed') {
// es. scarica subito il PDF firmato (link valido circa 1 ora)
const pdf = Buffer.from(await (await fetch(evt.signedPdfUrl)).arrayBuffer());
// … salva pdf, aggiorna il gestionale con evt.guid / evt.docNumber
}
res.sendStatus(200); // rispondere 2xx entro 5 secondi
});
app.listen(3000);// ASP.NET Core (minimal API)
using SignOtp;
using System.Text.Json;
var app = WebApplication.Create(args);
var secret = Environment.GetEnvironmentVariable("SIGNOTP_WEBHOOK_SECRET")!;
app.MapPost("/signotp/callback", async (HttpRequest req) =>
{
using var reader = new StreamReader(req.Body);
var rawBody = await reader.ReadToEndAsync();
if (!SignOtpWebhook.Verify(rawBody, req.Headers["X-SignOTP-Timestamp"], req.Headers["X-SignOTP-Signature"], secret))
return Results.Unauthorized();
var evt = JsonSerializer.Deserialize<SignedCallback>(rawBody, SignOtpClient.Json)!;
if (evt.Event == "document.signed")
{
// es. scarica subito il PDF firmato (link valido circa 1 ora)
using var http = new HttpClient();
var pdf = await http.GetByteArrayAsync(evt.SignedPdfUrl);
// … salva pdf, aggiorna il gestionale con evt.Guid / evt.DocNumber
}
return Results.Ok(); // rispondere 2xx entro 5 secondi
});
app.Run();Stati del documento
| Stato | Significato |
|---|---|
UPLOAD_PENDING | Pratica creata, PDF non ancora caricato o non ancora finalizzato. |
READY | Documento pronto: il cliente può aprire il link e firmare. |
OTP_SENT | Il cliente ha richiesto il codice: SMS inviato, in attesa della conferma. |
SIGNING | Codice corretto, generazione del PDF firmato in corso (pochi secondi). |
SIGNED | Documento firmato: il PDF firmato è disponibile. |
SUPERSEDED | Sostituito da un documento successivo della stessa pratica prima di essere firmato. |
Errori
In caso di errore la risposta ha un codice HTTP 4xx o 5xx e questo corpo:
{ "error": { "code": "UPLOAD_NOT_FOUND", "message": "File … non ancora caricato" } }
| HTTP | code | Quando |
|---|---|---|
| 400 | INVALID_JSON, VALIDATION_ERROR | Corpo della richiesta non valido |
| 401 | UNAUTHORIZED | x-api-key mancante, errata o disattivata |
| 404 | NOT_FOUND | Pratica o documento inesistente, o di un'altra applicazione |
| 409 | UPLOAD_NOT_FOUND, NOT_SIGNED, CONFLICT, SIGNING_IN_PROGRESS | Operazione non compatibile con lo stato del documento |
| 413 | PDF_TOO_LARGE | PDF oltre 20 MB |
| 422 | NOT_A_PDF, PDF_INVALID | File non PDF, danneggiato o protetto da password |
import { SignOtpError } from './signotp-client.mjs';
try {
await signotp.finalize(guid);
} catch (err) {
if (err instanceof SignOtpError && err.code === 'UPLOAD_NOT_FOUND') {
// il PDF non è stato caricato: ripetere l'upload
} else throw err;
}try
{
await signotp.FinalizeAsync(guid);
}
catch (SignOtpException ex) when (ex.Code == "UPLOAD_NOT_FOUND")
{
// il PDF non è stato caricato: ripetere l'upload
}Limiti
| Dimensione massima del PDF | 20 MB |
| Validità della URL di upload | 15 minuti |
| Testo del mittente | 5000 caratteri |
| Codice OTP | 6 cifre, valido 5 minuti, massimo 5 tentativi |
| SMS per documento | massimo 5, ad almeno 60 secondi l'uno dall'altro |
| Numeri di cellulare | Italia (+39) |
Client da scaricare
Domande o problemi di integrazione? Scrivi a info@signotp.com.