API Tendfolio
Introduzione
Superficie REST versionata per far parlare Tendfolio con gli strumenti che già usi: dashboard di agenzia, CI, script di manutenzione, Zapier o n8n. Legge lo stato della flotta e comanda le operazioni principali senza passare dal pannello.
Il contratto è stabile: una modifica che rompe i client esistenti non viene applicata a v1, va su una versione nuova.
https://tendfolio.com/api/v1
Autenticazione
Ogni richiesta porta la chiave in un header Authorization. Le chiavi si creano da Impostazioni → Chiavi API: serve il permesso org:manage (ruolo OWNER o ADMIN) e un piano che includa l'API pubblica.
curl -H "Authorization: Bearer tendfolio_a1b2c3d4e5f6_…" \ https://tendfolio.com/api/v1/me
Il valore in chiaro è mostrato una sola volta, alla creazione: a riposo conserviamo solo un hash SHA-256, quindi non è recuperabile nemmeno da noi. Per ruotare una chiave se ne crea una nuova, si aggiorna l'integrazione e si revoca la vecchia.
Modello di autorizzazione
Una chiave agisce per conto del membro che l'ha creata. I permessi effettivi sono l'intersezione fra gli scope concessi alla chiave e i permessi che quel membro possiede in quel momento nell'organizzazione. Tre conseguenze pratiche:
- una chiave non può mai fare più di chi l'ha creata;
- declassare o rimuovere quel membro disattiva le sue chiavi all'istante, senza doverle revocare a mano;
- lo scoping per-sito è ereditato: se il creatore vede 3 siti su 50, la chiave ne vede 3.
Una chiave non eredita mai i privilegi di amministratore di piattaforma, anche se il creatore li possiede.
Scope
Ogni chiave porta gli scope scelti alla creazione. Le rotte dichiarano quello che richiedono: senza, la risposta è 403 MISSING_SCOPE.
| Scope | Consente |
|---|---|
| site:read | Leggere i siti e il loro stato |
| site:create | Aggiungere siti |
| site:update | Modificare i siti |
| plugin:update | Aggiornare plugin e temi |
| core:update | Aggiornare WordPress (core) |
| plugin:manage | Attivare / disattivare plugin |
| security:read | Leggere i risultati di sicurezza |
| security:scan | Avviare scansioni |
| backup:create | Creare backup |
| backup:download | Scaricare backup |
| ai:read | Leggere i suggerimenti AI |
| ai:run | Avviare generazioni AI (consuma crediti) |
| content:manage | Creare articoli e media sui siti |
Restano deliberatamente fuori dall'API pubblica le operazioni che non dovrebbero avvenire senza una persona davanti: eliminazione di un sito, ripristino di un backup (sovrascrive il sito), 1-click login verso wp-admin, gestione del team e fatturazione.
Rate limit
120 richieste al minuto per chiave. Ogni risposta porta lo stato corrente della finestra; superato il limite arriva 429 con Retry-After.
X-RateLimit-Limit: 120 X-RateLimit-Remaining: 118 X-RateLimit-Reset: 1785000060
Errori
Formato uniforme, discriminabile sul campo code: è quello da usare nel codice. Il message è in inglese — l'API parla una lingua sola — ed è pensato per gli sviluppatori, quindi può cambiare.
{ "code": "MISSING_SCOPE", "message": "Missing scope for this key: backup:create." }| HTTP | code | Significato |
|---|---|---|
401 | MISSING_KEY | Header Authorization assente. |
401 | MALFORMED_KEY | La chiave non è nel formato tendfolio_<id>_<secret>. |
401 | INVALID_KEY | Chiave inesistente o segreto errato. |
401 | REVOKED_KEY | Chiave revocata dal pannello. |
401 | EXPIRED_KEY | Chiave oltre la data di scadenza. |
401 | ORPHANED_KEY | Il membro che ha creato la chiave non è più nell'organizzazione. |
403 | FEATURE_NOT_IN_PLAN | Il piano dell'organizzazione non include l'API pubblica. |
403 | MISSING_SCOPE | La chiave non ha lo scope richiesto dalla rotta. |
404 | SITE_NOT_FOUND | Sito inesistente oppure fuori dalla portata della chiave. |
429 | RATE_LIMITED | Limite di richieste superato. La risposta porta Retry-After. |
Su un sito fuori portata la risposta è 404, non 403: un 403 confermerebbe l'esistenza di quell'id a chi non dovrebbe conoscerlo.
Endpoint
Tutti i percorsi sono relativi a https://tendfolio.com/api/v1. Dove compare {siteId} va l'id restituito da GET /sites.
/mequalsiasi chiaveIdentità della chiave: organizzazione, piano, scope concessi. È l'endpoint da chiamare per primo quando si integra qualcosa — se risponde, chiave e piano sono a posto.
{
"organization": { "id": "clx…" },
"plan": { "id": "clx…", "name": "Agency", "slug": "agency" },
"key": { "id": "clx…", "name": "Integrazione monitoraggio", "scopes": ["site:read"] },
"siteCount": 42
}siteCountè il numero di siti visibili a questa chiave, non il totale dell'organizzazione: se chi l'ha creata vede 3 siti su 50, vale 3.
/sitessite:readElenco dei siti accessibili alla chiave, con stato di connessione, versioni e aggiornamenti in sospeso.
{
"items": [
{
"id": "clx…",
"url": "https://esempio.it",
"label": "Esempio",
"status": "CONNECTED",
"tags": ["cliente-a"],
"wpVersion": "6.8.1",
"phpVersion": "8.3",
"multisite": false,
"maintenanceMode": false,
"uptimeStatus": "UP",
"lastSeenAt": "2026-08-04T21:10:00.000Z",
"lastLatencyMs": 187,
"pendingUpdates": 3,
"createdAt": "2026-05-02T09:00:00.000Z"
}
]
}/sites/{siteId}site:readCome sopra per un singolo sito, più gli elenchi completi di plugin e temi.
{
"id": "clx…",
"url": "https://esempio.it",
"status": "CONNECTED",
"plugins": [
{
"slug": "woocommerce",
"name": "WooCommerce",
"version": "9.1.2",
"latestVersion": "9.2.0",
"active": true,
"updateAvailable": true
}
],
"themes": [ … ]
}/sites/{siteId}/updatessite:readSolo ciò che ha un aggiornamento disponibile: plugin, temi e core.
{
"items": [
{
"type": "plugin",
"slug": "woocommerce",
"name": "WooCommerce",
"currentVersion": "9.1.2",
"latestVersion": "9.2.0"
}
]
}/sites/{siteId}/updatesplugin:updateAccoda aggiornamenti. La risposta è immediata: il lavoro prosegue nel worker e compare nel log attività del sito, attribuito al membro che ha creato la chiave.
{
"items": [
{ "type": "plugin", "slug": "woocommerce" },
{ "type": "theme", "slug": "storefront" }
],
"backupFirst": true,
"safeUpdate": true
}{ "ok": true, "queued": 2 }backupFirst— backup del database prima di aggiornare (richiede la featurebackupsnel piano).safeUpdate— verifica il sito prima e dopo, con rollback automatico se si rompe. ImplicabackupFirst.- Un elemento di tipo
corerichiede in più lo scopecore:update.
/sites/{siteId}/securitysecurity:readUltima scansione e problemi ancora aperti — né risolti né ignorati.
{
"lastScan": {
"id": "clx…",
"status": "DONE",
"findingsCount": 2,
"startedAt": "2026-08-03T02:10:00.000Z",
"finishedAt": "2026-08-03T02:14:00.000Z"
},
"findings": [
{
"id": "clx…",
"severity": "HIGH",
"title": "Vulnerabilità nota in Contact Form 7",
"cve": "CVE-2026-1234",
"component": "contact-form-7",
"affectedVersion": "5.9.1",
"fixedVersion": "5.9.4",
"detectedAt": "2026-08-03T02:14:00.000Z"
}
]
}/sites/{siteId}/security/scansecurity:scanAccoda una scansione di sicurezza.
{ "ok": true }/sites/{siteId}/backupssite:readUltimi 50 backup, solo metadati.
{
"items": [
{
"id": "clx…",
"type": "DATABASE",
"status": "DONE",
"origin": "MANUAL",
"sizeBytes": "184320512",
"createdAt": "2026-08-04T03:00:00.000Z"
}
]
}- I token di download non sono mai esposti dall'API pubblica.
sizeBytesè una stringa: un backup può superare i 2^53 byte, oltre i quali un numero JSON perderebbe precisione.
/sites/{siteId}/backupsbackup:createAvvia un backup manuale.
{ "type": "DATABASE" }{ "ok": true }typeè opzionale:DATABASE(default),FILESoFULL.- Valgono la feature
backupsdel piano, la quota di backup manuali del mese e lo spazio disponibile: se manca qualcosa la risposta è 403 con ilcoderelativo.
/sites/{siteId}/articlescontent:manage + ai:runGenera un articolo con l'AI e lo deposita sul sito come bozza, pubblicato o programmato. Se ometti topicPrompt, l'argomento lo sceglie il sistema a partire dalle categorie e dagli articoli già pubblicati, evitando i doppioni.
{
"topicPrompt": "Come scegliere un commercialista per una startup",
"count": 3,
"length": "medium",
"tone": "informative",
"imageCount": 1,
"destination": "DRAFT",
"categories": ["Fisco"],
"tags": ["startup"]
}{
"id": "clx…",
"ids": ["clx…", "cly…", "clz…"],
"count": 3,
"requested": 3,
"status": "QUEUED",
"credits": 18,
"createdAt": "2026-08-07T09:12:00.000Z"
}- Asincrona: risponde subito con lo stato QUEUED, la generazione dura uno-due minuti. Si segue con GET sullo stesso id.
- Richiede la feature ai_content nel piano e il connettore ≥ 2.20.0 sul sito. I crediti (articolo 1/2/3/5/7 secondo la lunghezza, più 3 per immagine) sono addebitati alla richiesta e rimborsati se la generazione fallisce senza depositare nulla sul sito.
- Con count fino a 5 si generano più articoli in una volta: ognuno prende un argomento diverso, e se c'è topicPrompt il primo lo segue alla lettera mentre gli altri ne ricavano tagli distinti. Ogni articolo ha la sua prenotazione di crediti: se finiscono a metà gruppo, count nella risposta è minore di requested.
/sites/{siteId}/articles/{articleId}/statuscontent:managePubblica, riporta in bozza o programma un articolo già generato
{
"destination": "SCHEDULE",
"scheduledFor": "2026-08-14T07:00:00.000Z"
}{
"id": "clx…",
"destination": "SCHEDULE",
"scheduledFor": "2026-08-14T07:00:00.000Z",
"wpStatus": "future",
"wpPostUrl": "https://sito.it/?p=412"
}- Non costa crediti: il pezzo è già stato scritto e pagato. Si scrive prima su WordPress e solo dopo su Tendfolio, così il pannello non dice mai “pubblicato” mentre il sito mostra una bozza. Programmare richiede il connettore ≥ 2.21.0 e una data futura.
- Un articolo programmato lo pubblica WordPress, non Tendfolio. WP-Cron però scatta alla prima visita dopo l'orario, quindi su un sito senza traffico può non scattare: ogni dieci minuti Tendfolio controlla chi è rimasto indietro di oltre un quarto d'ora e lo pubblica mantenendo la data prevista. Chi nel frattempo è tornato in bozza non viene toccato.
/sites/{siteId}/articlessite:readUltimi 50 articoli generati per il sito, senza il corpo del testo.
[
{
"id": "clx…",
"status": "READY",
"title": "Come scegliere un commercialista per una startup",
"destination": "SCHEDULE",
"scheduledFor": "2026-08-14T07:00:00.000Z",
"wordCount": 1180,
"imageCount": 1,
"wpPostId": 412,
"wpPostUrl": "https://sito.it/?p=412",
"wpStatus": "future",
"creditsCharged": 6
}
]/sites/{siteId}/articles/{articleId}site:readIl singolo articolo con il testo completo e l'avanzamento della generazione.
{
"id": "clx…",
"status": "RUNNING",
"topic": "Come scegliere un commercialista per una startup",
"data": {
"steps": [
{ "key": "context", "status": "done" },
{ "key": "topic", "status": "done" },
{ "key": "text", "status": "running" },
{ "key": "images", "status": "pending" },
{ "key": "publish", "status": "pending" }
]
}
}- Gli step vanno in ordine: context → topic → text → images → publish. Lo stato passa da QUEUED a RUNNING a READY, oppure FAILED con il campo error valorizzato.
/sites/{siteId}/uptimesite:readStato del monitoraggio e incidente aperto, se c'è.
{
"siteId": "clx…",
"monitorEnabled": true,
"status": "UP",
"lastPingAt": "2026-08-04T21:11:00.000Z",
"lastLatencyMs": 187,
"failCount": 0,
"openIncident": null
}Note operative
- Server-to-server. Il CORS resta ristretto al dominio del pannello: una chiave non va messa in JavaScript di browser, dove sarebbe leggibile da chiunque apra gli strumenti di sviluppo.
- Solo HTTPS.
- Tracciabilità. Ogni chiave registra ultimo utilizzo e ultimo IP; creazione e revoca finiscono nell'audit log, e le azioni avviate via API compaiono nel log attività attribuite al membro creatore.
Pronto a integrare?
Crea una chiave dal pannello e verifica l'integrazione con una chiamata a /me.
