API Tendfolio

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.

Base URL
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.

Richiesta
curl -H "Authorization: Bearer helm_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.

ScopeConsente
site:readLeggere i siti e il loro stato
site:createAggiungere siti
site:updateModificare i siti
plugin:updateAggiornare plugin e temi
core:updateAggiornare WordPress (core)
plugin:manageAttivare / disattivare plugin
security:readLeggere i risultati di sicurezza
security:scanAvviare scansioni
backup:createCreare backup
backup:downloadScaricare backup
ai:readLeggere i suggerimenti AI
ai:runAvviare generazioni AI (consuma crediti)
content:manageCreare 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.

Header di risposta
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.

Errore
{ "code": "MISSING_SCOPE", "message": "Missing scope for this key: backup:create." }
HTTPcodeSignificato
401MISSING_KEYHeader Authorization assente.
401MALFORMED_KEYLa chiave non è nel formato helm_<id>_<secret>.
401INVALID_KEYChiave inesistente o segreto errato.
401REVOKED_KEYChiave revocata dal pannello.
401EXPIRED_KEYChiave oltre la data di scadenza.
401ORPHANED_KEYIl membro che ha creato la chiave non è più nell'organizzazione.
403FEATURE_NOT_IN_PLANIl piano dell'organizzazione non include l'API pubblica.
403MISSING_SCOPELa chiave non ha lo scope richiesto dalla rotta.
404SITE_NOT_FOUNDSito inesistente oppure fuori dalla portata della chiave.
429RATE_LIMITEDLimite 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.

GET/menessuno scope

Identità della chiave: organizzazione, piano, scope concessi. È l'endpoint da chiamare per primo quando si integra qualcosa — se risponde, chiave e piano sono a posto.

Risposta
{
  "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.
GET/sitesscope: site:read

Elenco dei siti accessibili alla chiave, con stato di connessione, versioni e aggiornamenti in sospeso.

Risposta
{
  "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"
    }
  ]
}
GET/sites/{siteId}scope: site:read

Come sopra per un singolo sito, più gli elenchi completi di plugin e temi.

Risposta
{
  "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": [ … ]
}
GET/sites/{siteId}/updatesscope: site:read

Solo ciò che ha un aggiornamento disponibile: plugin, temi e core.

Risposta
{
  "items": [
    {
      "type": "plugin",
      "slug": "woocommerce",
      "name": "WooCommerce",
      "currentVersion": "9.1.2",
      "latestVersion": "9.2.0"
    }
  ]
}
POST/sites/{siteId}/updatesscope: plugin:update

Accoda aggiornamenti. La risposta è immediata: il lavoro prosegue nel worker e compare nel log attività del sito, attribuito al membro che ha creato la chiave.

Corpo della richiesta
{
  "items": [
    { "type": "plugin", "slug": "woocommerce" },
    { "type": "theme", "slug": "storefront" }
  ],
  "backupFirst": true,
  "safeUpdate": true
}
Risposta
{ "ok": true, "queued": 2 }
  • backupFirst — backup del database prima di aggiornare (richiede la feature backups nel piano).
  • safeUpdate — verifica il sito prima e dopo, con rollback automatico se si rompe. Implica backupFirst.
  • Un elemento di tipo core richiede in più lo scope core:update.
GET/sites/{siteId}/securityscope: security:read

Ultima scansione e problemi ancora aperti — né risolti né ignorati.

Risposta
{
  "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"
    }
  ]
}
POST/sites/{siteId}/security/scanscope: security:scan

Accoda una scansione di sicurezza.

Risposta
{ "ok": true }
GET/sites/{siteId}/backupsscope: site:read

Ultimi 50 backup, solo metadati.

Risposta
{
  "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.
POST/sites/{siteId}/backupsscope: backup:create

Avvia un backup manuale.

Corpo della richiesta
{ "type": "DATABASE" }
Risposta
{ "ok": true }
  • type è opzionale: DATABASE (default), FILES o FULL.
  • Valgono la feature backups del piano, la quota di backup manuali del mese e lo spazio disponibile: se manca qualcosa la risposta è 403 con il code relativo.
POST/sites/{siteId}/articlesscope: content:manage + ai:run

Genera 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.

Corpo della richiesta
{
  "topicPrompt": "Come scegliere un commercialista per una startup",
  "count": 3,
  "length": "medium",
  "tone": "informative",
  "imageCount": 1,
  "destination": "DRAFT",
  "categories": ["Fisco"],
  "tags": ["startup"]
}
Risposta
{
  "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.
PATCH/sites/{siteId}/articles/{articleId}/statusscope: content:manage

Pubblica, riporta in bozza o programma un articolo già generato

Corpo della richiesta
{
  "destination": "SCHEDULE",
  "scheduledFor": "2026-08-14T07:00:00.000Z"
}
Risposta
{
  "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.
GET/sites/{siteId}/articlesscope: site:read

Ultimi 50 articoli generati per il sito, senza il corpo del testo.

Risposta
[
  {
    "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
  }
]
GET/sites/{siteId}/articles/{articleId}scope: site:read

Il singolo articolo con il testo completo e l'avanzamento della generazione.

Risposta
{
  "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.
GET/sites/{siteId}/uptimescope: site:read

Stato del monitoraggio e incidente aperto, se c'è.

Risposta
{
  "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.

API Tendfolio — riferimento /api/v1