Base URL installata: `https://wa.pepis.it/api/v1`. Ogni chiamata API richiede un token nominativo nell’header `Authorization: Bearer TOKEN`. I token non sono accettati nella query string. La documentazione è pubblica, ma non contiene credenziali o dati degli account. JSON UTF-8. Il contratto macchina completo è disponibile in `openapi.json`. Inviare le richieste da un backend server-to-server; il servizio non abilita CORS per pagine ospitate su altri domini.
L’amministratore accede al pannello su `https://wa.pepis.it/` e genera un token assegnandogli nome del servizio, scadenza (1–3653 giorni (circa 10 anni, inclusi gli anni bisestili)) e permessi. Il token è mostrato una sola volta: conservarlo nel secret manager o in una variabile d’ambiente del backend chiamante, mai nel codice pubblico, nei log o in una pagina browser. Creare un token diverso per ogni applicazione. Tutti i permessi assegnati valgono per tutti gli account WhatsApp di questa installazione, incluso `global`: non c’è isolamento per cliente/account.
- `read`: elenco e stato account, lettura messaggi/media e feed.
- `send`: invio di testo e risposte citate.
- `revoke`: richiesta di revoca dei messaggi inviati.
Un servizio tipico necessita di `read` e `send`; `revoke` è facoltativo. I token non permettono creazione/collegamento/scollegamento degli account, lettura QR, gestione di altri token o accesso al pannello. La revoca impedisce nuove richieste e chiude gli stream SSE del token; non annulla un invio già partito. I token scaduti ricevono 401. La scadenza degli stream è verificata ogni 5 secondi.
La password amministrativa non serve alle applicazioni terze. Per ruotare un token, generarne uno nuovo, aggiornare il servizio chiamante e revocare quello precedente. Il nome identifica il servizio dichiarato; chi possiede un token rubato può impersonare quel servizio fino alla revoca/scadenza.
Limiti applicativi predefiniti: 300 richieste/minuto per IP, 120/minuto per identità, 30 invii/minuto per identità, 3 stream SSE contemporanei per identità. I login sono limitati separatamente. Ulteriori limiti al reverse proxy e blocchi Fail2ban contrastano abusi e tentativi di autenticazione. HTTP 429 include `Retry-After`; attendere e applicare backoff con jitter, senza ripetizioni aggressive. Il firewall può bloccare temporaneamente un IP che insiste con credenziali non valide.
Con 401 verificare token, revoca e scadenza; con 403 verificare i permessi. Un errore di autenticazione non va ritentato in ciclo. Il servizio usa HTTPS: non disabilitare la verifica del certificato TLS. I download dei media sono limitati a 25 MiB e 60 secondi.
Questo servizio è un dispositivo WhatsApp collegato tramite QR: non registra autonomamente la SIM e non sostituisce il telefono come dispositivo principale. Questa precisazione vale sia per l’installazione LAN sia per quella su ns3.
Per evitare suoni, vibrazioni e notifiche WhatsApp sul telefono, disabilitare le notifiche dell’app nelle impostazioni del telefono. Il backend non può garantire il silenzio del dispositivo: presenza online e ricevute di lettura non sostituiscono questa impostazione, soprattutto in caso di disconnessione del backend.
La lettura automatica è attiva per i nuovi messaggi ricevuti da tutti gli account, inclusi quelli consegnati alla riconnessione. Il backend salva prima il messaggio e il relativo evento, poi invia a WhatsApp la ricevuta di lettura e la sincronizzazione della chat come letta. Lo storico già presente prima dell’attivazione non viene riletto automaticamente. Le conferme al mittente rispettano le impostazioni privacy WhatsApp.
Il campo `whatsappRead.status` vale `pending`, `retrying` o `submitted`. Quest’ultimo indica che entrambe le operazioni verso WhatsApp sono terminate senza errore, non una verifica sul telefono. `attempts` conta i tentativi; `receiptSentAt` e `submittedAt` sono date ISO; `nextAttemptAt`, quando presente, è una scadenza Unix in millisecondi. I tentativi falliti riprendono con attesa crescente fino a cinque minuti; la coda persiste ai riavvii e attende la riconnessione dell’account. Gli aggiornamenti compaiono come `message.updated` nel feed.
La lettura WhatsApp non elimina messaggi, non consuma il feed e non significa che una delle applicazioni abbia elaborato il messaggio. Ogni applicazione deve conservare autonomamente il proprio cursore e avanzarlo solo dopo l’elaborazione. Il campo `status` dei messaggi in ingresso resta separato dalla lettura automatica. Il permesso API `read` autorizza solo la consultazione; lo stato `read` dei messaggi inviati riguarda invece le ricevute del destinatario.
Riferimenti: [dispositivi collegati WhatsApp](https://faq.whatsapp.com/378279804439436/), [gestione notifiche](https://faq.whatsapp.com/iphone/troubleshooting/how-to-manage-your-notifications), [lettura e presenza in Baileys](https://github.com/WhiskeySockets/Baileys#reading-messages).
Un account reale è un collegamento indipendente a WhatsApp. `GET /accounts` elenca gli account e include sempre `{ "id": "global", "virtual": true }`. Gli ID reali usano lettere minuscole, cifre, trattino e underscore, massimo 48 caratteri; `global` è riservato. Alla creazione, gli spazi iniziali/finali vengono rimossi e le maiuscole dell’ID convertite in minuscole; usare nelle chiamate successive l’ID restituito dal server.
`global` può essere usato negli endpoint di invio, elenco messaggi, feed polling e SSE. Aggrega tutti gli account. Ogni messaggio/evento espone l'account reale mediante `accountId`.
Per inviare attraverso `global`, omettere `accountId` nel body per selezionare a rotazione uno degli account collegati; specificarlo per forzare il mittente. La rotazione riparte al riavvio e non garantisce affinità per destinatario. La scelta può far apparire al destinatario numeri mittenti diversi: per una conversazione continuativa impostare l'account. Non c'è failover automatico dopo un invio incerto, per evitare duplicati.
Per leggere, scaricare o revocare un singolo messaggio tramite `global`, aggiungere `?accountId=ID_REALE`. Un ID messaggio è un riferimento opaco: conservare la coppia `accountId` + `id` e non ricostruire gli ID dal numero di telefono.
| Metodo | Percorso | Uso |
| GET | /accounts | Elenco, stato, account globale |
| POST | /accounts | Crea account: `{ "id":"personale", "name":"Personale" }` |
| GET | /accounts/{accountId} | Stato dell'account; per global, account collegati |
| POST | /accounts/{accountId}/connect | Avvia o riprende collegamento, HTTP 202 |
| GET | /accounts/{accountId}/qr | QR come stringa e `dataUrl` PNG; ripetere se cambia |
| POST | /accounts/{accountId}/disconnect | Logout e rimozione sessione locale |
Creazione, collegamento, scollegamento e QR richiedono una sessione amministrativa; le mutazioni richiedono anche l’header CSRF e l’origine corretta, gestiti dal pannello. Elenco e stato sono disponibili con token `read`. Gli ultimi tre endpoint richiedono un account reale. Per il QR, una risposta 409 significa che non è disponibile in quel momento: leggere lo stato. Stati possibili: `disconnected`, `connecting`, `qr`, `connected`, `reconnecting`, `logged_out`, `connection_replaced`, `disconnecting`, `error`. Il QR viene aggiornato da WhatsApp: quello precedente può scadere. Il server salva la sessione e la ripristina senza QR finché resta valida.
`POST /accounts/{accountId}/messages`
Header obbligatori: `Authorization: Bearer TOKEN` e `Idempotency-Key: identificativo-unico-operazione`. Usare un UUID generato una sola volta per ogni invio desiderato, conservandolo anche nei retry.
{
"to": "+393331234567",
"text": "Ciao, confermi l'appuntamento?",
"accountId": "personale",
"replyTo": "ID_OPACO_MESSAGGIO_ORIGINALE"
}
`to` e `text` sono obbligatori. `to` accetta un numero internazionale senza spazi (con o senza `+`) oppure il `chatId` restituito dall'API. Sono supportati JID individuali `@s.whatsapp.net`, `@lid` e gruppi esistenti `@g.us`. Nessuna risoluzione per nome della rubrica. Il testo ammette fino a 20000 caratteri. `accountId` e `replyTo` sono opzionali; per una risposta citata tramite `global`, `accountId` è obbligatorio e deve essere quello che ha ricevuto il messaggio. `replyTo` deve riferirsi a un messaggio salvato della stessa chat/account.
Esempio invio globale con scelta automatica:
curl -X POST https://wa.pepis.it/api/v1/accounts/global/messages \
-H "Authorization: Bearer $WA_API_TOKEN" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: ordine-123-notifica-1' \
-d '{"to":"+393331234567","text":"Il tuo ordine è pronto"}'
Risposta HTTP 202:
{
"accountId": "personale",
"replayed": false,
"message": {
"id": "ID_OPACO",
"whatsappId": "ID_WHATSAPP",
"chatId": "393331234567@s.whatsapp.net",
"direction": "outgoing",
"timestamp": 1791979200,
"type": "conversation",
"text": "Il tuo ordine è pronto",
"status": "submitted",
"source": "api",
"media": null
}
}
202 significa che il tentativo è stato registrato: verificare `message.status`, non equivale a consegna. Stati: `sending` in corso; `submitted` trasporto completato; `server_ack` accettazione osservata dal server WhatsApp; `delivered` consegna; `read` lettura; `played` riproduzione; `error` errore riportato da WhatsApp; `unknown` esito incerto (timeout/errore di rete/riavvio); `received` messaggio in ingresso. Per i gruppi, `receipts` espone le ricevute per partecipante senza attribuire una lettura a tutto il gruppo. Le ricevute dipendono da WhatsApp e dalle impostazioni del destinatario; non tutte sono garantite.
Ripetere la stessa richiesta sulla stessa URL con la stessa chiave restituisce HTTP 200, `replayed:true` e lo stato corrente, senza inviare nuovamente. Per `global` viene mantenuto l'account scelto. Riutilizzare la chiave con contenuto differente restituisce 409. Le chiavi sono persistenti e separate per servizio (token) e scope URL: non cambiare da account reale a `global`, né token, durante un retry. Gli invii espongono `serviceId` per attribuirli al servizio. Non ritentare un invio incerto con un token appena ruotato. `unknown` non autorizza un reinvio automatico con chiave diversa; verificare prima lo stato. Il servizio non accoda invii per account offline.
| Metodo | Percorso | Uso |
| GET | /accounts/{accountId}/messages | Storico: `chatId` opzionale, `limit` 1–500 (100), `offset` (0) |
| GET | /accounts/{accountId}/messages/{messageId} | Messaggio e stato corrente |
| GET | /accounts/{accountId}/messages/{messageId}/media | Scarica media ricevuto; account connesso richiesto |
| GET | /accounts/{accountId}/events | Polling persistente: `after` (0), `limit` 1–500 (100) |
| GET | /accounts/{accountId}/events/stream | SSE con replay: `after` o header `Last-Event-ID` |
Lo storico è ordinato per timestamp decrescente e non è uno snapshot: mentre arrivano messaggi la paginazione per offset può spostarsi. Per integrazioni affidabili usare il feed. Sono conservati solo i messaggi consegnati al servizio: non viene promessa l'importazione dell'intera cronologia del telefono. Sono incluse le chat archiviate; gli stati `status@broadcast` sono esclusi.
Risposta polling:
{
"events": [{
"id": 42,
"accountId": "personale",
"type": "message.created",
"time": "2026-10-14T10:00:00.000Z",
"data": {
"id": "ID_OPACO", "whatsappId": "ID_WHATSAPP",
"chatId": "393331234567@s.whatsapp.net", "participant": null,
"direction": "incoming", "timestamp": 1791972000,
"type": "conversation", "text": "Confermo",
"status": "received", "source": "live", "media": null
}
}],
"nextCursor": 42,
"hasMore": false
}
Tipi di evento: `message.created`, `message.updated`, `account.updated`. Gli ultimi contengono `data.state`. Il feed contiene anche invii e aggiornamenti: per nuove risposte ricevute filtrare `type === "message.created"` e `data.direction === "incoming"`. Per escludere i messaggi sincronizzati filtrare anche `data.source === "live"`; `sync` indica consegne di sincronizzazione, `api` invii del servizio.
Persistire `nextCursor` solo dopo aver elaborato gli eventi; se `hasMore` è true leggere subito la pagina successiva, altrimenti attendere ad esempio 2 secondi. Il cursore è numerico e persistente, con possibili salti per account. Non riutilizzare il cursore di un account come punto di partenza di un feed differente. Ogni consumatore ha il proprio cursore, nessuna lettura cancella eventi. Deduplicare gli effetti per ID evento: se un consumatore si arresta prima di salvare il cursore riceverà nuovamente gli eventi.
Per SSE inviare lo stesso header `Authorization: Bearer TOKEN` usato nel polling. Usare un client HTTP streaming server-to-server, per esempio `curl -N -H "Authorization: Bearer $WA_API_TOKEN" https://wa.pepis.it/api/v1/accounts/global/events/stream?after=0`. Il server invia `id`, `event` e `data` JSON, con heartbeat ogni 15 secondi e replay dei successivi eventi. `Last-Event-ID` prevale su `after`. Gli eventi hanno un nome: il client deve elaborare `event` e `data`. EventSource nativo del browser non permette l’header Bearer: per le applicazioni terze usare un backend, senza inserire il token nell’URL. Il feed globale usa `.../accounts/global/events/stream`.
I media ricevuti espongono MIME e nome originale quando disponibili. Il download dipende dalla disponibilità presso WhatsApp: non è un archivio permanente degli allegati. Reaction e altri tipi possono comparire con `type` specifico e `text` vuoto; non sono normalizzati in contenuto testuale.
1. `GET /accounts/{accountId}/messages/{messageId}/revocation` restituisce `canRequest`, `reason`, `expiresAt`, `policyHours`, `guaranteed:false` e lo stato dell'eventuale richiesta.
2. `DELETE /accounts/{accountId}/messages/{messageId}` richiede la cancellazione per tutti.
3. Consultare messaggio/feed per gli aggiornamenti.
Per `global`, aggiungere `?accountId=ID_REALE` a entrambe le chiamate. Sono revocabili dal servizio solo messaggi propri, conservati con i dati WhatsApp necessari. Il limite predefinito è una finestra locale prudenziale di 48 ore, configurabile con `REVOKE_WINDOW_HOURS`: non è un limite interrogato o garantito da WhatsApp. `canRequest:true` indica solo che il servizio può tentare; WhatsApp può rifiutare la revoca per altri motivi.
Stati `revocation.status`: `requesting`, `requested` (richiesta trasmessa), `observed` (evento di revoca osservato), `unknown` (esito non determinabile). HTTP 202 e `requested` non provano la rimozione dai dispositivi destinatari. Anche `observed` non garantisce che il destinatario non abbia già letto o salvato il contenuto. Non tutti i rifiuti di WhatsApp sono osservabili: nessuna falsa conferma di successo. Chiamate DELETE ripetute restituiscono lo stato registrato senza una seconda richiesta. La revoca non elimina il testo dal database né dal feed locale; questa API non offre cancellazione locale della cronologia.
Formato: `{ "error": { "code": "ACCOUNT_NOT_CONNECTED", "message": "Account non collegato" } }`.
| HTTP | Codici principali | Comportamento client |
| 400 | INVALID_ACCOUNT, INVALID_TEXT, INVALID_RECIPIENT, INVALID_REPLY, INVALID_ARGUMENT, IDEMPOTENCY_KEY_REQUIRED, REPLY_ACCOUNT_REQUIRED, ACCOUNT_REQUIRED, ACCOUNT_MISMATCH, VIRTUAL_ACCOUNT | Correggere richiesta |
| 401 | UNAUTHORIZED | Verificare il token; non ritentare in ciclo |
| 403 | FORBIDDEN, CSRF_FAILED, CROSS_ORIGIN_DENIED | Verificare permessi/origine |
| 429 | RATE_LIMITED, AUTH_BLOCKED, STREAM_LIMIT | Rispettare Retry-After e limitare la concorrenza |
| 404 | ACCOUNT_NOT_FOUND, MESSAGE_NOT_FOUND, REPLY_NOT_FOUND, MEDIA_NOT_FOUND, NOT_FOUND | Verificare riferimenti |
| 409 | ACCOUNT_EXISTS, ACCOUNT_NOT_CONNECTED, ACCOUNT_BUSY, NO_CONNECTED_ACCOUNT, QR_UNAVAILABLE, IDEMPOTENCY_CONFLICT, NOT_OUTGOING, MESSAGE_NOT_AVAILABLE, LOCAL_WINDOW_EXPIRED | Risolvere stato/limite |
| 413 | body troppo grande | Ridurre richiesta |
| 500 | INTERNAL_ERROR | Verificare servizio; per invii ritentare solo con stessa chiave |
import json, os, urllib.request
BASE = 'https://wa.pepis.it/api/v1'
def request(path, body=None, key=None):
headers = {'Content-Type': 'application/json',
'Authorization': 'Bearer ' + os.environ['WA_API_TOKEN']}
if key:
headers['Idempotency-Key'] = key
req = urllib.request.Request(BASE + path,
data=json.dumps(body).encode() if body is not None else None,
headers=headers)
with urllib.request.urlopen(req, timeout=40) as response:
return json.load(response)
page = request('/accounts/global/events?after=0&limit=100')
for event in page['events']:
if event['type'] != 'message.created':
continue
message = event['data']
if message['direction'] != 'incoming' or message['source'] != 'live':
continue
print(event['accountId'], message['chatId'], message['text'])
# Esempio di risposta esplicita: invocare solo se desiderata.
# result = request('/accounts/global/messages', {
# 'accountId': event['accountId'], 'to': message['chatId'],
# 'replyTo': message['id'], 'text': 'Ricevuto, grazie!'
# }, key='risposta-evento-' + str(event['id']))
# Salvare page['nextCursor'] in modo persistente dopo l'elaborazione.
1. Ricevere dall’amministratore un token dedicato con `read` e `send`, scadenza concordata e `revoke` solo se necessario.
2. Configurare `WA_API_BASE_URL=https://wa.pepis.it/api/v1` e `WA_API_TOKEN` nel backend.
3. Verificare con `GET /accounts` e scegliere un account reale o `global`.
4. Per inviare, salvare prima una chiave di idempotenza stabile e chiamare `POST /accounts/{accountId}/messages`.
5. Per ricevere, leggere `GET /accounts/global/events?after=CURSORE`; elaborare e poi salvare `nextCursor` in modo persistente. Filtrare gli eventi `message.created`, `incoming`, `live`.
6. Per rispondere tramite `global`, usare l’`accountId` dell’evento, `to=data.chatId` e facoltativamente `replyTo=data.id`.
7. Gestire 401/403 senza loop, 429 con attesa e gli esiti `unknown` senza invii duplicati. Pianificare la rotazione prima della scadenza del token.
La suite automatica usa un trasporto simulato e non certifica il collegamento reale del tuo telefono, la consegna a un destinatario o l'accettazione delle revoche. Questi passaggi richiedono il QR e una prova concordata con un destinatario. Nessun account esistente è riutilizzato automaticamente.
[Baileys — protocollo e revoca](https://github.com/WhiskeySockets/Baileys/blob/master/README.md) · [WhatsApp — eliminare messaggi](https://faq.whatsapp.com/1370476507114859/).
I messaggi individuali in ingresso espongono `senderPhone` (formato `+` e cifre) quando il trasporto WhatsApp consente di risolvere il numero. Sono utilizzati il JID telefonico o l’alias autenticato del JID LID; mai testo o nome del profilo. Se il campo è null, non considerare il mittente verificato e non interpretare le cifre di un `@lid` come numero telefonico. I gruppi restituiscono null.