API documentation · v1

Your fleet, from your code

List sites, apply updates, start backups and scans from scripts, CI, Zapier or n8n. A key with the permissions you choose and a contract that doesn’t shift under your feet.

The first five minutes

From key to first response

  1. Create a key

    In Settings → API keys choose what it can do: read only, or also update and back up. The value is shown once.

    site:readplugin:updatebackup:create
  2. Call /me

    It’s the first call to make: if it answers, the key works and your plan includes the API.

    Request
    curl https://tendfolio.com/api/v1/me \
      -H "Authorization: Bearer tendfolio_…"
  3. Read your sites

    Everything else starts here: updates, backups, security, uptime and articles, site by site.

    GET /sites · 200
    { "items": [
      { "label": "Pasticceria Bianchi",
        "pendingUpdates": 3 }
    ] }

All endpoints

All paths are relative to https://tendfolio.com/api/v1. Where {siteId} appears, use the id returned by GET /sites.

EndpointWhat it doesScope
Account
GET/meThe key's identity: organization, plan, granted scopes. This is the first endpoint to call when wiring up an integration — if it answers, key and plan are fine.any key
Sites
GET/sitesThe sites this key can reach, with connection status, versions and pending updates.site:read
GET/sites/{siteId}As above for a single site, plus the full plugin and theme lists.site:read
Updates
GET/sites/{siteId}/updatesOnly what has an update available: plugins, themes and core.site:read
POST/sites/{siteId}/updatesQueues updates. The response is immediate: the work continues in the worker and shows up in the site's activity log, attributed to the member who created the key.plugin:update
Security
GET/sites/{siteId}/securityLatest scan and issues still open — neither fixed nor ignored.security:read
POST/sites/{siteId}/security/scanQueues a security scan.security:scan
Backups
GET/sites/{siteId}/backupsThe last 50 backups, metadata only.site:read
POST/sites/{siteId}/backupsStarts a manual backup.backup:create
Uptime
GET/sites/{siteId}/uptimeMonitoring status and the open incident, if there is one.site:read
Articles
POST/sites/{siteId}/articlesGenerates an article with AI and places it on the site as a draft, published or scheduled. Omit topicPrompt and the system picks the subject itself, from the site categories and what it has already published, avoiding duplicates.content:manage + ai:run
PATCH/sites/{siteId}/articles/{articleId}/statusPublish, move back to draft, or schedule an article already generatedcontent:manage
GET/sites/{siteId}/articlesLast 50 generated articles for the site, without the body text.site:read
GET/sites/{siteId}/articles/{articleId}A single article with the full text and the generation progress.site:read

Scopes per key

Each key does only what you grant it: a read-only key can’t update anything.

120 requests a minute

Per key. Beyond that you get 429 with Retry-After: wait and try again.

Errors with a code

Every error carries a stable code field to branch on in your code, besides the message.

Everything is logged

What you do through the API shows up in the activity log, under the member who created the key.

Full reference

Tendfolio API

Introduction

A versioned REST surface that lets Tendfolio talk to the tools you already use: agency dashboards, CI, maintenance scripts, Zapier or n8n. Read the state of your fleet and trigger the main operations without opening the panel.

The contract is stable: a change that breaks existing clients is never applied to v1 — it goes into a new version.

Base URL
https://tendfolio.com/api/v1

Authentication

Every request carries the key in an Authorization header. Keys are created in Settings → API keys: you need the org:manage permission (OWNER or ADMIN role) and a plan that includes the public API.

Request
curl -H "Authorization: Bearer tendfolio_a1b2c3d4e5f6_…" \
  https://tendfolio.com/api/v1/me

The plaintext value is shown only once, at creation: at rest we keep just an SHA-256 hash, so not even we can recover it. To rotate a key, create a new one, update the integration, then revoke the old one.

Authorization model

A key acts on behalf of the member who created it. Effective permissions are the intersection of the scopes granted to the key and the permissions that member holds at that moment in the organization. Three practical consequences:

  • a key can never do more than the person who created it;
  • demoting or removing that member disables their keys instantly, with no need to revoke them by hand;
  • per-site scoping is inherited: if the creator sees 3 sites out of 50, the key sees 3.

A key never inherits platform administrator privileges, even if its creator has them.

Scopes

Each key carries the scopes chosen at creation. Routes declare what they require: without it, the response is 403 MISSING_SCOPE.

ScopeAllows
site:readRead sites and their status
site:createAdd sites
site:updateEdit sites
plugin:updateUpdate plugins and themes
core:updateUpdate WordPress (core)
plugin:manageActivate / deactivate plugins
security:readRead security results
security:scanStart scans
backup:createCreate backups
backup:downloadDownload backups
ai:readRead AI suggestions
ai:runRun AI generations (spends credits)
content:manageCreate articles and media on sites

Deliberately outside the public API are the operations that shouldn't happen without a person present: deleting a site, restoring a backup (it overwrites the site), 1-click login into wp-admin, team management and billing.

Rate limit

120 requests per minute per key. Every response reports the current state of the window; past the limit you get 429 with Retry-After.

Response headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1785000060

Errors

Uniform shape, discriminated on the code field: that's the one to branch on in code. The message is English — the API speaks one language — and is written for developers, so it may change.

Error
{ "code": "MISSING_SCOPE", "message": "Missing scope for this key: backup:create." }
HTTPcodeMeaning
401MISSING_KEYAuthorization header missing.
401MALFORMED_KEYThe key isn't in the tendfolio_<id>_<secret> format.
401INVALID_KEYKey does not exist, or wrong secret.
401REVOKED_KEYKey revoked from the panel.
401EXPIRED_KEYKey past its expiry date.
401ORPHANED_KEYThe member who created the key is no longer in the organization.
403FEATURE_NOT_IN_PLANThe organization's plan doesn't include the public API.
403MISSING_SCOPEThe key lacks the scope the route requires.
404SITE_NOT_FOUNDSite does not exist, or is out of the key's reach.
429RATE_LIMITEDRequest limit exceeded. The response carries Retry-After.

For a site out of reach the response is 404, not 403: a 403 would confirm that id exists to someone who shouldn't know it.

Endpoints

All paths are relative to https://tendfolio.com/api/v1. Where {siteId} appears, use the id returned by GET /sites.

GET/meany key

The key's identity: organization, plan, granted scopes. This is the first endpoint to call when wiring up an integration — if it answers, key and plan are fine.

Response
{
  "organization": { "id": "clx…" },
  "plan": { "id": "clx…", "name": "Agency", "slug": "agency" },
  "key": { "id": "clx…", "name": "Integrazione monitoraggio", "scopes": ["site:read"] },
  "siteCount": 42
}
  • siteCount is the number of sites visible to this key, not the organization's total: if its creator sees 3 sites out of 50, it reads 3.
GET/sitessite:read

The sites this key can reach, with connection status, versions and pending updates.

Response
{
  "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}site:read

As above for a single site, plus the full plugin and theme lists.

Response
{
  "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}/updatessite:read

Only what has an update available: plugins, themes and core.

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

Queues updates. The response is immediate: the work continues in the worker and shows up in the site's activity log, attributed to the member who created the key.

Request body
{
  "items": [
    { "type": "plugin", "slug": "woocommerce" },
    { "type": "theme", "slug": "storefront" }
  ],
  "backupFirst": true,
  "safeUpdate": true
}
Response
{ "ok": true, "queued": 2 }
  • backupFirst — back up the database before updating (requires the backups feature in the plan).
  • safeUpdate — checks the site before and after, with automatic rollback if it breaks. Implies backupFirst.
  • An item of type core additionally requires the core:update scope.
GET/sites/{siteId}/securitysecurity:read

Latest scan and issues still open — neither fixed nor ignored.

Response
{
  "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/scansecurity:scan

Queues a security scan.

Response
{ "ok": true }
GET/sites/{siteId}/backupssite:read

The last 50 backups, metadata only.

Response
{
  "items": [
    {
      "id": "clx…",
      "type": "DATABASE",
      "status": "DONE",
      "origin": "MANUAL",
      "sizeBytes": "184320512",
      "createdAt": "2026-08-04T03:00:00.000Z"
    }
  ]
}
  • Download tokens are never exposed by the public API.
  • sizeBytes is a string: a backup can exceed 2^53 bytes, past which a JSON number would lose precision.
POST/sites/{siteId}/backupsbackup:create

Starts a manual backup.

Request body
{ "type": "DATABASE" }
Response
{ "ok": true }
  • type is optional: DATABASE (default), FILES or FULL.
  • The plan's backups feature, the monthly manual-backup quota and available storage all apply: if any is missing the response is 403 with the relevant code.
POST/sites/{siteId}/articlescontent:manage + ai:run

Generates an article with AI and places it on the site as a draft, published or scheduled. Omit topicPrompt and the system picks the subject itself, from the site categories and what it has already published, avoiding duplicates.

Request body
{
  "topicPrompt": "Come scegliere un commercialista per una startup",
  "count": 3,
  "length": "medium",
  "tone": "informative",
  "imageCount": 1,
  "destination": "DRAFT",
  "categories": ["Fisco"],
  "tags": ["startup"]
}
Response
{
  "id": "clx…",
  "ids": ["clx…", "cly…", "clz…"],
  "count": 3,
  "requested": 3,
  "status": "QUEUED",
  "credits": 18,
  "createdAt": "2026-08-07T09:12:00.000Z"
}
  • Asynchronous: it answers immediately with status QUEUED, generation takes one or two minutes. Follow it with GET on the same id.
  • Requires the ai_content plan feature and connector ≥ 2.20.0 on the site. Credits (1/2/3/5/7 for the article depending on length, plus 3 per image) are charged on request and refunded if generation fails without placing anything on the site.
  • With count up to 5 you generate several articles at once: each takes a different topic, and when topicPrompt is set the first follows it literally while the others derive distinct angles. Every article has its own credit reservation: if credits run out mid-batch, count in the response is lower than requested.
PATCH/sites/{siteId}/articles/{articleId}/statuscontent:manage

Publish, move back to draft, or schedule an article already generated

Request body
{
  "destination": "SCHEDULE",
  "scheduledFor": "2026-08-14T07:00:00.000Z"
}
Response
{
  "id": "clx…",
  "destination": "SCHEDULE",
  "scheduledFor": "2026-08-14T07:00:00.000Z",
  "wpStatus": "future",
  "wpPostUrl": "https://sito.it/?p=412"
}
  • No credits: the piece is already written and paid for. WordPress is updated first and Tendfolio second, so the panel never says "published" while the site still shows a draft. Scheduling requires connector ≥ 2.21.0 and a future date.
  • A scheduled article is published by WordPress, not by Tendfolio. WP-Cron only fires on the first visit after the slot, so on a site without traffic it may never fire: every ten minutes Tendfolio checks which posts are more than fifteen minutes late and publishes them, keeping their intended date. Anything moved back to draft is left alone.
GET/sites/{siteId}/articlessite:read

Last 50 generated articles for the site, without the body text.

Response
[
  {
    "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}site:read

A single article with the full text and the generation progress.

Response
{
  "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" }
    ]
  }
}
  • Steps run in order: context → topic → text → images → publish. Status goes QUEUED → RUNNING → READY, or FAILED with the error field set.
GET/sites/{siteId}/uptimesite:read

Monitoring status and the open incident, if there is one.

Response
{
  "siteId": "clx…",
  "monitorEnabled": true,
  "status": "UP",
  "lastPingAt": "2026-08-04T21:11:00.000Z",
  "lastLatencyMs": 187,
  "failCount": 0,
  "openIncident": null
}

Operational notes

  • Server-to-server. CORS stays restricted to the panel's domain: a key must never be put in browser JavaScript, where anyone opening devtools could read it.
  • HTTPS only.
  • Traceability. Every key records last use and last IP; creation and revocation land in the audit log, and actions started via the API appear in the activity log attributed to the member who created the key.

Ready to integrate?

Create a key in the panel and verify the integration with a call to /me.

Tendfolio API — /api/v1 reference