Documentazione API Backend

Chiamate che il PHP fa verso Piano.io e verso il sistema OTP interno

Indice

1. API Piano 1b. Newsletter Piano ESP 1c. Feed newsletter (Quotidiano) 1d. Contenuti suggeriti (Cxense) 2. API OTP (interne) 3. User Collector 4. Mappa Frontend → Backend

1. API Piano

Integrazione Piano.io. Config in config/config.php → piano. Header Authorization = api_token (dove previsto). Sandbox: https://sandbox.piano.io.

Chiave configUso
aidApplication ID in query/body
api_tokenToken publisher
base_urlIdentity: /id/api/v1/publisher/identity
user_update_url/api/v3/publisher/user/update
user_get_v3_url/api/v3/publisher/user/get (profilo + custom fields)
users_get_url/id/api/v1/publisher/users/get (legacy identity)
token_url/id/api/v1/publisher/token
user_search_url/api/v3/publisher/user/search
user_access_list_url/api/v3/user/access/list (Bearer access token utente)
identity_token_url/id/api/v1/identity/token (OAuth refresh)

1b. Newsletter — Piano ESP (iscrizione)

Iscrizione mailing list via Piano ESP. Config: config.php → newsletter. URL: GET https://api-esp.piano.io/tracker/securesub/email/{email}/ml/{mlids}?api_key=...

TestataChiave configapi_key
Quotidiano Nazionaleqnnewsletter.api_keys.qn
il Resto del Carlinordcnewsletter.api_keys.rdc
Il Giornogionewsletter.api_keys.gio
La Nazionenaznewsletter.api_keys.naz

Piano ESP GET /tracker/securesub/email/{email}/ml/{mlids}

Action FE: newsletter_subscribe. Preferito selections: [{feed, mlid}, …] da feed misti: il PHP raggruppa per testata e applica la api_key corretta. Retrocompat: testata + mlids.

GET .../email/user%40mail.it/ml/3832?api_key=***
GET .../email/user%40mail.it/ml/5906,3832?api_key=***

1c. Feed newsletter (Quotidiano — non Piano)

Wrapper verso i feed WordPress. Non passano da Piano. Action FE: newsletter_feed. Con all: true (o senza feed) restituisce i 4 feed + items[] normalizzati.

feedUpstream
qn…/feed/qn
ilresto…/feed/ilresto
ilgiorno…/feed/ilgiorno
lanazione…/feed/lanazione

1d. Contenuti suggeriti — Piano / Cxense widget

Wrapper verso POST https://api.cxense.com/public/widget/data. Config: config.php → recommendations. Action FE: recommendations.

Chiave configUso
recommendations.api_urlEndpoint Cxense widget data
recommendations.widget_id_homeSenza URL articolo, toptrend=false
recommendations.widget_id_articleCon URL articolo, toptrend=false
recommendations.widget_id_home_toptrendSenza URL, toptrend=true
recommendations.widget_id_article_toptrendCon URL, toptrend=true
recommendations.widget_idAlias legacy (fallback se widget_id_article vuoto)
recommendations.user_id_prefixChiave in user.ids (default usi)
recommendations.timeoutTimeout cURL (default 10s)
recommendations.debugSe false: body = solo { "response": { "items": […] } } (campi Cxense originali). Se true: wrapper API + articles mappati + recommendations_debug

Cxense POST /public/widget/data

{
  "widgetId": "<WIDGET_ID selezionato>",
  "context": { "url": "https://www.example.com/articolo" },  // omesso se home
  "user": { "ids": { "usi": "<UDI>" } }   // opzionale
}

FE recommendations: url opzionale, toptrend opzionale (default false). Risposta normalizzata: articles[] con title (da og-title), url, thumbnail, click_url, description (troncata, da og-description/description). Opzionali (solo se presenti): author, publishtime, categoria, tassonomia, keyword, kw-classification, kw-taxonomy, kw-category, kw-sentiment, category0, category1, createtime.

Piano POST /id/api/v1/publisher/identity/login

Query: aid, email, password=%22%22 (passwordless) oppure password reale. Body JSON: aid, email, stay_logged_in ("true"/"false"), eventuale password.

POST .../identity/login?aid=...&email=user%40mail.it&password=%22%22
{
  "aid": "...",
  "email": "user@mail.it",
  "stay_logged_in": "false"
}

Piano POST /id/api/v1/publisher/identity/register

Query include il parametro sandbox pasword=null, is_passwordless=true, consents, is_magic_link_sent, email, aid.

{
  "password": "",
  "is_passwordless": true,
  "is_magic_link_sent": true,
  "email": "user@mail.it",
  "aid": "..."
}

Piano POST /id/api/v1/publisher/identity/doi/status

Query: aid, uid, email_confirmation_state=CONFIRMED. Body: email_confirmation_state: "confirmed".

{
  "aid": "...",
  "uid": "PNIxxxx",
  "email_confirmation_state": "confirmed"
}

Piano POST /id/api/v1/publisher/users/get

Usato da get_user_email: lookup per email in query (aid, api_token, email). Poi arricchimento con v3 user/get per custom fields.

Piano POST /api/v3/publisher/user/get

Form-urlencoded. Usato da get_user (uid) per leggere profilo e custom fields (otp_phone, city, birth_date). Query/body: aid, api_token, uid.

Piano POST /api/v3/publisher/user/search

Form-urlencoded. Cerca per custom field otp_phone.

api_token=...
aid=...
source=CF
offset=0
limit=100
custom_fields=[{"field_name":"otp_phone","data_type":"NUMBER","condition":{"type":"EQUAL","equal":"3331234567"}}]

Piano POST /api/v3/publisher/user/update — identity

Se il frontend passa first_name / last_name / email. Form-urlencoded + query aid, api_token, uid.

Piano POST /api/v3/publisher/user/update — custom fields

Se il frontend passa otp_phone / city / birth_date. Query: aid, api_token, uid. Body JSON:

{
  "city": "Roma",
  "birth_date": "1973-07-21",
  "otp_phone": 331222222
}

Content-Type: application/json. Le due update partono dalla stessa action frontend update_profile.

Piano POST /id/api/v1/publisher/token

Query: api_token, aid, uid. Body JSON opzionale. Restituisce access_token salvato in cookie dal backend.

Piano GET /api/v3/user/access/list

Elenco delle risorse a cui l’utente ha accesso. Auth con Bearer access token (non api_token publisher). Config: piano.user_access_list_url. Action FE: user_access_list.

GET https://sandbox.piano.io/api/v3/user/access/list?aid=<AID>
Authorization: Bearer <ACCESS_TOKEN>

Piano POST /id/api/v1/identity/token (refresh)

Rinnova un access token scaduto con il refresh_token OAuth. Form-urlencoded. client_id = AID. Per il refresh non servono client_secret né redirect_uri. Config: piano.identity_token_url. Action FE: refresh_token.

POST https://sandbox.piano.io/id/api/v1/identity/token
Content-Type: application/x-www-form-urlencoded

client_id=<AID>
grant_type=refresh_token
refresh_token=<REFRESH_TOKEN>
{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "refresh_token": "RFbqr...",
  "expires_in": 3600,
  "email_confirmation_required": false
}

Nota: il flusso attuale publisher/token (access_token / QN*) tipicamente non restituisce un refresh_token; questo endpoint serve quando il token arriva dal flusso OAuth identity.

2. API OTP (interne)

Non sono endpoint Piano: gestione OTP locale in lib/otp.php + invio in lib/notify.php e lib/sms.php. Config in otp, mail, sms.

Chiave configDefault / uso
otp.ttl_seconds300 — durata validità codice
otp.length6 cifre
otp.resend_cooldown_seconds60 — non rinviare se già inviato da meno di N secondi
otp.debugonly debug — se true, le risposte API includono il codice OTP in chiaro (data.otp); in produzione lasciare false
mail.*SMTP (disabilitato se from_address vuoto)
sms.*Gateway QNMedia lib/sms.php → POST awssns_sms

OTP Generate

Tabella otp_codes: uid, otp, scadenza, created_at.

OTP Verify

Esiti: ok / invalid / expired. In caso di successo il record viene cancellato (one-shot).

OTP SMS Gateway (lib/sms.php)

POST verso sms.api_url con Bearer sms.api_key.

POST https://api.staging.quotidiano.net/sms-gateway/awssns_sms
Authorization: Bearer {api_key}
Content-Type: application/json

{
  "receiver": "+393331234567",
  "body": "QNMedia Access: il tuo codice OTP è 1234. Scade tra 5 minuti.",
  "sender": "QNMedia"
}

receiver e body sono dinamici (telefono normalizzato E.164 + template otp_sms.txt).

OTP Notifiche

3. User Collector

Ingest login dopo emissione token verso Quotidiano staging (uid + accessToken). Lib: lib/user_collector.php. Config: config.php → user_collector. Integrato nelle API QN* all’emissione del token. Action debug: login-ingestion-user-collector.

Chiave configUso
base_urlhttps://api.staging.quotidiano.net/user-collector
ingest_login_path/ingest/login
internal_tokenHeader x-internal-token
timeoutTimeout cURL (default 20s)

User Collector POST /ingest/login

Payload minimo: solo uid e accessToken (niente Piano access/list).

POST https://api.staging.quotidiano.net/user-collector/ingest/login
accept: */*
x-internal-token: {internal_token}
Content-Type: application/json

{
  "uid": "google-oauth2|104497752255509497539",
  "accessToken": "sddadsa"
}

Action debug login-ingestion-user-collector: richiede uid + accessToken (oppure access_token / cookie __utp / Bearer) e inoltra lo stesso body al Collector.

4. Mappa Frontend → Backend

Action FETipoServizi chiamati dal PHP
otp_generateOTPDB OTP + email/SMS
verify_otpOTP (+ Piano)DB OTP; se registrazione → Piano DOI
otp_verify_localOTPSolo DB OTP (test)
loginPianoPiano login; se passwordless anche OTP
registerPianoPiano register + OTP
get_userPianoPiano user/get v3 per uid → profilo ridotto
get_user_emailPianoidentity users/get by email + user/get v3 → profilo ridotto
search_userPianoPiano user/search
update_profilePianoPiano user/update (1 o 2 volte)
doi_confirmPianoPiano doi/status
access_tokenPianoPiano publisher/token + cookie
refresh_tokenPianoPOST identity/token (grant_type=refresh_token) + cookie opzionale
user_access_listPianoGET Piano user/access/list con Bearer access token
QNVerifyOtp / QNRegister / QNUpdateProfileQN* + User Collectordopo access_token → ingest/login {uid, accessToken}
login-ingestion-user-collectorUser Collector (debug)stesso ingest {uid, accessToken}, solo per test
newsletter_subscribePiano ESPGET api-esp.piano.io/tracker/securesub/…
newsletter_feedFeed QNWrapper newsletters.quotidiano.net/wp-json/feed/…
recommendationsCxensePOST api.cxense.com/public/widget/data
test_mailDebugSMTP test con steps diagnostici (vedi pagina Test)