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.

Base URLhttps://api.signotp.com
FormatoJSON su HTTPS
Autenticazioneheader x-api-key
DatiAWS 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

  1. Crei la pratica con POST /v1/requests e ricevi un guid e una URL di upload.
  2. Carichi il PDF con un PUT diretto sulla URL di upload.
  3. Finalizzi con POST /v1/requests/{guid}/finalize e ricevi il signUrl.
  4. Invii il signUrl al cliente, per esempio via email: SignOTP non invia email.
  5. Il cliente apre il link, legge il PDF, inserisce il cellulare e conferma con il codice OTP ricevuto via SMS.
  6. 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

POST/v1/requests

Avvia 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.

CampoTipoDescrizione
textstringMessaggio del mittente mostrato nella pagina di firma (\n per andare a capo), max 5000 caratteri. Obbligatorio per una nuova pratica.
emailstringEmail del mittente, mostrata al cliente. Obbligatoria per una nuova pratica.
callbackUrlstringFacoltativo. URL https:// chiamata quando il documento viene firmato.
existingUidstringFacoltativo. 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

PUT{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.pdf

Finalizza

POST/v1/requests/{guid}/finalize

Verifica 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/Xk3P9aQ2mZr7Lw0Bv5Nt8c
var 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

GET/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

GET/v1/requests/{guid}/documents/{n}/signed

Risponde 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:

HeaderValore
X-SignOTP-Eventdocument.signed
X-SignOTP-Timestampistante dell'invio, in secondi Unix
X-SignOTP-Signaturesha256= 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 2xx entro 5 secondi; altrimenti SignOTP ritenta una sola volta. L'esito resta visibile in documents[].callback.
  • signedPdfUrl si scarica senza autenticazione per circa un'ora; downloadUrl è permanente e richiede x-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

StatoSignificato
UPLOAD_PENDINGPratica creata, PDF non ancora caricato o non ancora finalizzato.
READYDocumento pronto: il cliente può aprire il link e firmare.
OTP_SENTIl cliente ha richiesto il codice: SMS inviato, in attesa della conferma.
SIGNINGCodice corretto, generazione del PDF firmato in corso (pochi secondi).
SIGNEDDocumento firmato: il PDF firmato è disponibile.
SUPERSEDEDSostituito 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" } }
HTTPcodeQuando
400INVALID_JSON, VALIDATION_ERRORCorpo della richiesta non valido
401UNAUTHORIZEDx-api-key mancante, errata o disattivata
404NOT_FOUNDPratica o documento inesistente, o di un'altra applicazione
409UPLOAD_NOT_FOUND, NOT_SIGNED, CONFLICT, SIGNING_IN_PROGRESSOperazione non compatibile con lo stato del documento
413PDF_TOO_LARGEPDF oltre 20 MB
422NOT_A_PDF, PDF_INVALIDFile 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 PDF20 MB
Validità della URL di upload15 minuti
Testo del mittente5000 caratteri
Codice OTP6 cifre, valido 5 minuti, massimo 5 tentativi
SMS per documentomassimo 5, ad almeno 60 secondi l'uno dall'altro
Numeri di cellulareItalia (+39)

Client da scaricare

Domande o problemi di integrazione? Scrivi a info@signotp.com.