Convenzioni
- Metodo: POST,
Content-Type: application/json - URL:
/?action=<nome>(DirectoryIndex / rewrite;index.phpresta il front controller) - Specifica OpenAPI 3: openapi.yaml · openapi.json · UI: swagger.html
- Postman: importa
postman_collection.json(file Collection v2.1, consigliato). In alternativaopenapi.json. - Risposta base:
{ "success": true|false, "error": "...", "data": { ... } } - In debug (
config.php → debug: true):piano_debug/piano_debugs; per APIQN*anchedebug[]con le chiamate interne - Codice OTP in chiaro nelle risposte (
data.otp): solo seconfig.php → otp.debug: true(only debug)
API ottimizzate (FE)
QNLogin
QNVerifyOtp
QNRegister
QNAddPhone
QNUpdateProfile
API OTP
otp_generate
verify_otp
otp_verify_local
API Piano / profilo
search_user
get_user_email
get_user
login
register
update_profile
doi_confirm
access_token
refresh_token
user_access_list
User Collector
login-ingestion-user-collector
Newsletter
newsletter_subscribe
newsletter_feed
Contenuti suggeriti
recommendations
0. API ottimizzate FE (QN*)
Meno round-trip. Flusso classico in /mockup; FE ottimizzato in /mockupfe. Dettaglio: flusso-fe.html.
POST action=QNLogin
Telefono → #13 OTP solo SMS → #14. Email → #32 OTP email → #33 (la ricerca utente #30/#31 avviene dopo #34).
{ "identifier": "3331234567" }
{ "identifier": "user@example.com" }
Output: next_step, flow, oggetto otp (metadati invio). Campo otp.otp in chiaro: only debug (otp.debug). debug[] solo se config.debug.
POST action=QNVerifyOtp
Verifica OTP. Se login completo → token (next_step: 90). Altrimenti prossimo step.
Quando emette access_token, il backend esegue anche User Collector ingest/login con { uid, accessToken }. Vedi debug[] step login_ingestion.
phone_register + pending_proof: #24 → #24b register_proof → next_step: 25.
email_verify: #34 → #30/#31 → se nuovo next_step: 37; se esiste con telefono #65/#90; se esiste senza telefono next_step: 61.
{
"otp_code": "1234", // only debug — in sandbox; in prod arriva da SMS/email
"flow": "phone",
"phone": "3331234567",
"email": "user@example.com",
"uid": "",
"stay_logged_in": true
}
POST action=QNRegister
Fasi con proof HMAC (no sessione). Il commit phase=register richiede anche i dati profilo e chiude con token (#90).
// #22 — solo OTP email (dopo phone_proof); consensi a #25
{
"phase": "email_otp",
"email": "new@mail.it",
"phone": "3331234567",
"phone_proof": "...",
"flow": "phone_register"
}
// #21 / #35 — register + consensi + DOI (#36) + update (#40+#42) + token
{
"phase": "register",
"email": "new@mail.it",
"phone": "3331234567",
"register_proof": "...",
"consents": { "terms_conditions": true },
"first_name": "Luca",
"last_name": "Rossi",
"birth_date": "1990-01-15",
"city": "Milano",
"flow": "email_register"
}
phase=email_otp→next_step: 23+pending_proofphase=register→ register + DOI (#36) + update (#40+#42) → token + User Collector ingest →next_step: 90
POST action=QNAddPhone
Step #61–#62: controlla che il telefono non sia già registrato; se libero invia OTP SMS.
{
"phone": "3331234567",
"uid": "PNI...",
"email": "user@mail.it",
"flow": "email_add_phone"
}
Se già usato: success: false, error: "Telefono già registrato su un altro account", data.reason: "phone_taken".
Ok: next_step: 62, screen: otp_phone, payload otp.
POST action=QNUpdateProfile
Unisce update_profile + access_token. Restituisce access_token e imposta cookie __utp.
Una sola chiamata FE aggiorna sia identity (first_name/last_name) sia custom (otp_phone, city, birth_date) — copre #40 e #42. Nei flussi register nuovi questi campi passano già in QNRegister phase=register.
Dopo access_token il backend esegue User Collector ingest (come QNVerifyOtp / QNRegister).
Prima di salvare otp_phone verifica unicità: se il numero appartiene a un altro uid → errore phone_taken.
{
"uid": "PNI...",
"first_name": "Luca",
"last_name": "Rossi",
"birth_date": "1990-01-15",
"city": "Milano",
"otp_phone": "3331234567",
"stay_logged_in": true
}
{
"success": true,
"data": {
"next_step": 90,
"done": true,
"access_token": "...",
"screen": "success"
}
}
1. API OTP
Gestione codice one-time locale (DB + invio email/SMS). Usate per login telefono, rinvio codice e verifica.
OTP POST action=otp_generate
Invia un OTP (email e/o SMS). Non rinvia se già inviato da meno di resend_cooldown_seconds (default 60).
// telefono nuovo — solo SMS
{
"uid": "3331234567",
"phone": "3331234567"
}
// utente esistente con otp_phone — SMS + email
{
"uid": "3331234567",
"phone": "3331234567",
"notify_email": "user@example.com"
}
uid obbligatorio (chiave OTP in DB). phone per SMS. notify_email opzionale: se presente invia anche l’email (utente già registrato con telefono verificato).
// ok
{
"success": true,
"data": {
"otp": "123456", // only debug — presente solo se otp.debug=true
"resent": true,
"retry_after": 60,
"cooldown": 60,
"expires_in": 300,
"notified": { "email": false, "sms": true }
}
}
// già inviato
{
"success": false,
"error": "OTP già inviato. Riprova tra 42 secondi.",
"data": {
"blocked": true,
"reason": "already_sent",
"retry_after": 42,
"cooldown": 60
}
}
OTP POST action=verify_otp
Verifica l’OTP locale. Se is_registration e c’è uid, conferma anche il DOI Piano.
{
"email": "3331234567",
"otp_code": "123456", // only debug — esempio sandbox; in prod arriva da SMS/email
"is_registration": false,
"uid": "PNIxxxx",
"access_token": "",
"stay_logged_in": false
}
// ok
{ "success": true, "data": { "valid": true, "doi": null, "access_token": null } }
// scaduto
{ "success": false, "error": "OTP scaduto. Richiedi un nuovo codice.", "data": { "valid": false, "reason": "expired" } }
// errato
{ "success": false, "error": "OTP non valido.", "data": { "valid": false, "reason": "invalid" } }
OTP POST action=otp_verify_local
Solo test: verifica OTP nel DB senza DOI/sessione.
{ "uid": "3331234567", "otp_code": "123456" /* only debug — esempio sandbox */ }
{ "success": true, "data": { "valid": true, "reason": "ok" } }
2. API Piano / profilo
Identity, ricerca utente, profilo, DOI e access token. Il backend traduce queste action in chiamate Piano.io.
Piano POST action=search_user
Cerca utente per telefono (otp_phone). Usato all’inizio del percorso telefono (prima dell’OTP) e dopo verify_otp.
{ "phone": "3331234567" }
{
"success": true,
"data": { /* risposta Piano search */ },
"meta": {
"found": true,
"count": 1,
"uid": "PNIxxxx",
"email": "user@example.com",
"has_otp_phone": true,
"otp_phone": "3331234567"
}
}
Piano POST action=get_user_email
Lookup utente per email (percorso login email). Chiama Piano identity users/get + profilo v3.
{ "email": "user@example.com" }
Stessa risposta di get_user.
Piano POST action=get_user
Profilo ridotto per uid (non restituisce il payload grezzo Piano).
{ "uid": "PNIxxxx" }
{
"success": true,
"data": {
"uid": "PNIxxxx",
"email": "user@example.com",
"first_name": "Luca",
"last_name": "Rossi",
"city": "Roma",
"birth_date": "1973-07-21",
"otp_phone": "3331234567",
"has_otp_phone": true,
"profile_complete": true
},
"meta": {
"exists": true,
"uid": "PNIxxxx",
"has_otp_phone": true,
"otp_phone": "3331234567",
"profile_complete": true
}
}
Piano POST action=login
Login Piano. Senza password è passwordless (può anche generare OTP). Con password è il ramo email senza telefono.
// passwordless
{ "email": "user@example.com", "stay_logged_in": false }
// con password
{ "email": "user@example.com", "password": "secret", "stay_logged_in": true }
{ "success": true, "data": { /* risposta Piano login */ } }
Piano POST action=register
Registrazione passwordless su Piano. Con send_otp: false non invia OTP (usato dopo verifica email nel percorso nuovo utente).
{
"email": "user@example.com",
"phone": "3331234567",
"consents": { "terms_conditions": true },
"is_magic_link_sent": true,
"send_otp": true
}
{
"success": true,
"data": {
"access_token": "eyJ...",
"pub_id": "PNIxxxx",
"otp": "123456" // only debug — solo se otp.debug=true
}
}
Piano POST action=update_profile
Una sola chiamata frontend. Il backend fa 1 o 2 chiamate Piano in base ai campi valorizzati. Se è presente otp_phone, prima verifica che non sia già usato da un altro utente (phone_taken).
{
"uid": "PNIxxxx",
"email": "user@example.com",
"first_name": "Luca",
"last_name": "Rossi",
"otp_phone": "331222222",
"city": "Roma",
"birth_date": "1973-07-21"
}
Campi identity: first_name, last_name, email. Custom: otp_phone, city, birth_date.
{
"success": true,
"payload": { "uid": "PNIxxxx", "first_name": "Luca", "city": "Roma" },
"data": {
"calls": ["identity", "custom"],
"identity": { /* Piano */ },
"custom": { /* Piano */ }
}
}
Piano POST action=doi_confirm
Imposta DOI email a CONFIRMED (dopo verifica OTP in registrazione, se non già fatto da verify_otp).
{ "uid": "PNIxxxx" }
Piano POST action=access_token
Emette token Piano e lo salva in cookie __utp (anche via JS sul frontend autenticato).
{ "uid": "PNIxxxx", "stay_logged_in": true }
{ "success": true, "data": { "access_token": "eyJ..." } }
Piano POST action=refresh_token
Rinnova un access token scaduto tramite Piano POST /id/api/v1/identity/token (grant_type=refresh_token). Se Piano restituisce un nuovo access_token, aggiorna anche il cookie __utp.
Non usato dal mockup FE per ora. Serve quando si dispone di un refresh_token OAuth (non tipicamente restituito da publisher/token).
{ "refresh_token": "RFbqr...", "stay_logged_in": true }
{
"success": true,
"data": {
"access_token": "eyJhbGci...",
"token_type": "Bearer",
"refresh_token": "RFbqr...",
"expires_in": 3600,
"email_confirmation_required": false
}
}
Piano POST action=user_access_list
Dopo access_token: restituisce le risorse a cui l’utente ha accesso (Piano GET /api/v3/user/access/list). Il backend inoltra la chiamata con Authorization: Bearer <ACCESS_TOKEN>.
Token accettato da: body access_token, cookie __utp, oppure header Authorization: Bearer ….
{ "access_token": "eyJ..." }
{
"success": true,
"data": {
"count": 1,
"accesses": [
{
"access_id": "...",
"resource": { "rid": "RXXXX", "name": "Premium" },
"granted": true
}
],
"raw": { /* risposta Piano originale */ }
}
}
Nel mockup (success.html) viene chiamata automaticamente dopo il login.
POST action=login-ingestion-user-collector
Invia al User Collector (/user-collector/ingest/login) solo uid + accessToken (niente Piano access/list). Config: user_collector.
{
"uid": "google-oauth2|104497752255509497539",
"accessToken": "eyJ..."
}
Non è una chiamata FE di flusso. L’ingest è automatico dentro QNVerifyOtp / QNRegister / QNUpdateProfile quando generano l’access token. Questa action resta solo per debug (pagina Test).
Accetta anche access_token / cookie __utp / Bearer. In risposta: payload (token mascherato) + debug.
Piano ESP POST action=newsletter_subscribe
Iscrive l’email. Preferito il batch con selections[]: ogni voce porta feed + mlid (o testata + mlid). Il backend raggruppa per testata e usa la api_key corretta.
{
"email": "user@mail.it",
"selections": [
{ "feed": "ilresto", "mlid": "6521" },
{ "feed": "qn", "mlid": "3832" },
{ "feed": "lanazione", "mlid": "5919" }
]
}
// singola testata (retrocompat)
{
"email": "user@mail.it",
"testata": "rdc",
"mlids": "6521"
}
POST action=newsletter_feed
Senza feed (o all: true) restituisce i 4 feed in una sola risposta, più items[] piatti già pronti per la UI (con feed, testata, mlid).
{ "all": true }
{
"success": true,
"data": {
"items": [
{ "id": "ilresto:6521", "feed": "ilresto", "testata": "rdc", "mlid": "6521", "label": "… — Bologna" }
],
"feeds": { "ilresto": { /* raw */ }, "qn": {}, "ilgiorno": {}, "lanazione": {} },
"errors": {}
}
}
Singolo feed: { "feed": "ilresto" }.
Cxense POST action=recommendations
Contenuti suggeriti (widget Piano/Cxense). url e udi opzionali. toptrend opzionale (default false). Selezione automatica del widget:
- senza
url,toptrend=false→widget_id_home - con
url,toptrend=false→widget_id_article - senza
url,toptrend=true→widget_id_home_toptrend - con
url,toptrend=true→widget_id_article_toptrend
widget_id in input sovrascrive la selezione automatica.
{
"url": "https://www.quotidiano.net/cronaca/articolo-esempio", // opzionale
"udi": "PNI...", // opzionale
"toptrend": false // opzionale, default false
}
{
"success": true,
"data": {
"success": true,
"count": 2,
"pageUrl": "https://www.quotidiano.net/cronaca/articolo-esempio",
"userIdentified": true,
"toptrend": false,
"has_article_url": true,
"widget_id": "fd97d7…",
"articles": [
{
"title": "Titolo da og-title",
"url": "https://…",
"thumbnail": "https://…",
"click_url": "https://…",
"description": "Anteprima testo…",
"author": "GABRIELE MORONI",
"publishtime": "2026-10-04T00:00:00.000Z",
"categoria": "Cronaca",
"tassonomia": "…",
"keyword": "…",
"kw-classification": "…",
"kw-taxonomy": "…",
"kw-category": "…",
"kw-sentiment": "positive",
"category0": "…",
"category1": "…",
"createtime": "…"
}
]
}
}
Mockup: /mockupfe/recommendations.html.
Sequenza tipica frontend
| Percorso | Chiamate in ordine |
|---|---|
| Telefono, utente esiste | search_user → otp_generate (SMS o SMS+email) → verify_otp → search_user → access_token |
| Telefono, nuovo utente | otp_generate → verify_otp → search_user → register → update_profile → access_token |
| Email non registrata | get_user_email → otp_generate (email) → verify_otp → register + doi_confirm → OTP telefono → update_profile (phone) → update_profile (anagrafica) → access_token |
| Email con otp_phone | get_user_email → poi stesso flusso telefono |
| Email senza telefono | get_user_email → login (password) → otp_generate → verify_otp → update_profile → access_token |