Documentazione API Frontend

Chiamate JSON dal client verso /?action=... (senza index.php in URL).

Convenzioni

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"
}

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:

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

PercorsoChiamate 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