Helm API

A versioned REST surface that lets Helm 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 helm_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 helm_<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/meno scope

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/sitesscope: site: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}scope: 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}/updatesscope: site: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}/updatesscope: plugin: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}/securityscope: security: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/scanscope: security:scan

Queues a security scan.

Response
{ "ok": true }
GET/sites/{siteId}/backupsscope: site: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}/backupsscope: backup: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}/articlesscope: content: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}/statusscope: content: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 Helm 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 Helm. WP-Cron only fires on the first visit after the slot, so on a site without traffic it may never fire: every ten minutes Helm 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}/articlesscope: site: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}scope: 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}/uptimescope: site: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.

Helm API — /api/v1 reference · Helm