Persilio Persilio by Savo Group

Developers

Your data, anywhere you need it

Persilio captures every lead together with the full story of how it found you — channel, referrer, UTM parameters, click IDs, and the visitor's page-by-page journey. The REST API and MCP server give you that data programmatically: wire it into your reporting, your warehouse, or your AI agents.

On this page

Overview

The Persilio API is a REST API over HTTPS with JSON responses. It exposes your leads — including the complete acquisition attribution and visitor journey behind each one — your lead and traffic analytics, and, for organizations with a Persilio-managed website, the website CMS: read and edit pages as content blocks and publish the site.

Base URL:   https://api.persilio.com/api/v1
MCP server: https://api.persilio.com/mcp
Endpoint Description
GET/me Verify your key and identify the connected organization.
GET/leads List leads newest-first with full acquisition attribution; filterable and paginated.
GET/leads/{id} One lead in full: contact, activity history, and the tracked visitor journey.
GET/analytics/leads Aggregate lead analytics for a time range.
GET/analytics/traffic Aggregate website traffic analytics for a time range.
GET/cms/sites List CMS-managed websites with each site’s block vocabulary.
GET/cms/pages List a site’s pages; add ?slug= to fetch one page in full.
GET/cms/pages/{id} One page’s full draft: blocks + the field schema they follow.
POST/cms/pages Create a draft page, optionally from a template.
POST/cms/pages/{id} Update a page’s title, SEO, or full block array.
POST/cms/pages/{id}/blocks Add, edit, or remove a single block.
POST/cms/publish Publish draft changes: commit to GitHub + redeploy the site.
GET/cms/publishes/{id} Publish progress: committed → building → deployed / failed.

Authentication

Every request is authenticated with an org-scoped API key. Create one in the dashboard under Settings → API access (owners and managers only). The key is shown once at creation — store it like a password. Keys can be revoked at any time from the same screen, and revocation takes effect immediately.

Send the key as a bearer token on every request:

Authorization: Bearer psk_live_4f8a2c…
  • A key only ever sees the organization it was created in.
  • Keys are stored hashed — Persilio cannot show you a key again after creation.
  • A key acts as one person and carries scopes that are the dashboard's own areas — leads, contacts, messages, billing, analytics, reports, reviews, cms, seo and the rest of the sidebar. A key can never reach more than its holder can open in the dashboard, and it follows any change to that person's access on the Team page. A scoped key only reaches endpoints covered by its scopes, so a build-server key can render reviews without ever being able to read customer PII. A key with no scopes has everything its holder has.
  • The leads and analytics surfaces are read-only. Writes live behind explicit scopes: cms edits website content, billing creates and edits quotes, invoices, templates and e-signature links, and contacts manages the contact book.

Rate limits

Each key may make 120 requests per minute with bursts up to 30. The MCP server draws from the same budget. Over the limit, requests return 429 with a Retry-After header (in seconds) — wait that long and retry.

HTTP/2 429
Retry-After: 12

{ "error": { "message": "Rate limit exceeded.", "status": 429 } }

Conventions & pagination

  • Timestamps are Unix epoch milliseconds everywhere — request parameters and response fields alike.
  • Optional fields with no value are omitted from responses, not returned as null. Code defensively: lead.gclid ?? null.
  • IDs are opaque strings. Don't parse them; store and compare them as-is.
  • Dates in analytics (byDay) are UTC calendar days.

Pagination

GET /leads paginates with an opaque numeric cursor. Fetch the first page normally; while the response's nextCursor is non-null, request the next page with cursor=<nextCursor>. When it comes back null, you have everything.

# page 1
curl -s "https://api.persilio.com/api/v1/leads?limit=100" -H "Authorization: Bearer $KEY"
# → { "leads": [ …100 leads… ], "nextCursor": 1750912345678 }

# page 2
curl -s "https://api.persilio.com/api/v1/leads?limit=100&cursor=1750912345678" -H "Authorization: Bearer $KEY"
# → { "leads": [ …remaining leads… ], "nextCursor": null }

Endpoints

GET/me

Verifies your key and returns the organization it belongs to. Use it as a connectivity and auth check when setting up an integration.

Request

No parameters.

curl -s https://api.persilio.com/api/v1/me \
  -H "Authorization: Bearer psk_live_…"

Response

Field Type Description
orgId string The organization's id. All data returned by this key belongs to this org.
org.name string Organization display name.
org.domain string The org's website domain as configured in Persilio.
org.websiteUrl string? Full website URL, when set.
org.businessPhone string? Primary business phone, when set.
key.name string The name this API key was given when created.
{
  "orgId": "kd701nhcc2qedbce39g2qjzp8x826em9",
  "org": {
    "name": "Example HVAC Co",
    "domain": "example-hvac.com",
    "websiteUrl": "https://www.example-hvac.com",
    "businessPhone": "+15551234567"
  },
  "key": { "name": "Reporting script" }
}

GET/leads

Lists leads newest-first. Every lead includes its full first-touch attribution — channel, referrer, landing page, UTM parameters, and click IDs. See the lead object for all fields.

Request

Parameter Type Description
status string Filter by lead status. See Attribution values for the full list (new, contacted, estimated, scheduled, closed, lost, …).
channel string Filter by resolved acquisition channel: organic, ppc, social, referral, direct, or ai.
query string Free-text match against the lead name, email, or phone.
createdAfter number Only leads created after this Unix timestamp in milliseconds.
createdBefore number Only leads created before this Unix timestamp in milliseconds.
limit number Page size, 1–100. Default 25.
cursor number Pagination cursor — pass the nextCursor value from the previous page.
# Most recent 50 leads
curl -s "https://api.persilio.com/api/v1/leads?limit=50" \
  -H "Authorization: Bearer psk_live_…"

# New PPC leads from June 2026
curl -s "https://api.persilio.com/api/v1/leads?status=new&channel=ppc&createdAfter=1780272000000&createdBefore=1782864000000" \
  -H "Authorization: Bearer psk_live_…"

# Search by name / email / phone
curl -s "https://api.persilio.com/api/v1/leads?query=jane" \
  -H "Authorization: Bearer psk_live_…"

Response

Field Type Description
leads lead[] Array of lead objects — see The lead object below for every field.
nextCursor number | null Non-null when more results may exist. Pass it back as cursor to fetch the next page; stop when null.
{
  "leads": [
    {
      "id": "kd74x8m2p9qfz3hcvw6t5n1jrb89kd2e",
      "createdAt": 1751212345678,
      "cameInAt": 1751212345678,
      "name": "Jane Example",
      "email": "jane@example.com",
      "phone": "5551234567",
      "message": "Our AC went out yesterday — looking for a repair quote this week.",
      "status": "contacted",
      "source": "website_form",
      "sourceLabel": "AC Repair",
      "channel": "ppc",
      "referrer": "https://www.google.com/",
      "landingPage": "/services/ac-repair",
      "sourceUrl": "https://www.example-hvac.com/services/ac-repair",
      "utmSource": "google",
      "utmMedium": "cpc",
      "utmCampaign": "summer-ac-repair",
      "utmTerm": "ac repair near me",
      "utmContent": "ad-variant-b",
      "gclid": "EAIaIQobChMI8p2c4vHq9gIVxxWtBh2QkQAxEAAYASAAEgL",
      "sessionId": "b3f09c41-77aa-4c1e-9f2d-2f4e8d1c6a55",
      "visitorId": "9a1c3f7e-d2b8-4e06-8f31-5c7a2e9d4b10",
      "contactId": "s17k2m9w4rvq8n3xzp6c1f5hjd89tm3a",
      "pipelineStage": "Contacted",
      "notes": ["Called back — wants an estimate Thursday morning."],
      "lastActivityAt": 1751215600000
    }
  ],
  "nextCursor": 1750912345678
}

GET/leads/{id}

Returns one lead in full: every field from the lead object, plus the linked contact, the activity history (up to 50 entries, newest first), and — when the lead came through a tracked website — the journey object.

Request

Path parameter: the lead id from GET /leads. No query parameters.

curl -s https://api.persilio.com/api/v1/leads/kd74x8m2p9qfz3hcvw6t5n1jrb89kd2e \
  -H "Authorization: Bearer psk_live_…"

Response

The complete shape (optional fields with no value are omitted in real responses):

{
  "id": "kd74x8m2p9qfz3hcvw6t5n1jrb89kd2e",
  "createdAt": 1751212345678,
  "cameInAt": 1751212345678,
  "name": "Jane Example",
  "email": "jane@example.com",
  "phone": "5551234567",
  "message": "Our AC went out yesterday — looking for a repair quote this week.",
  "status": "contacted",
  "source": "website_form",
  "sourceLabel": "AC Repair",
  "channel": "ppc",
  "referrer": "https://www.google.com/",
  "landingPage": "/services/ac-repair",
  "sourceUrl": "https://www.example-hvac.com/services/ac-repair",
  "utmSource": "google",
  "utmMedium": "cpc",
  "utmCampaign": "summer-ac-repair",
  "utmTerm": "ac repair near me",
  "utmContent": "ad-variant-b",
  "gclid": "EAIaIQobChMI8p2c4vHq9gIVxxWtBh2QkQAxEAAYASAAEgL",
  "sessionId": "b3f09c41-77aa-4c1e-9f2d-2f4e8d1c6a55",
  "visitorId": "9a1c3f7e-d2b8-4e06-8f31-5c7a2e9d4b10",
  "contactId": "s17k2m9w4rvq8n3xzp6c1f5hjd89tm3a",
  "pipelineStage": "Contacted",
  "notes": ["Called back — wants an estimate Thursday morning."],
  "lastActivityAt": 1751215600000,
  "contact": {
    "id": "s17k2m9w4rvq8n3xzp6c1f5hjd89tm3a",
    "name": "Jane Example",
    "firstName": "Jane",
    "lastName": "Example",
    "email": "jane@example.com",
    "phone": "5551234567",
    "company": "Example Property Management"
  },
  "activities": [
    { "type": "status_change", "content": "Status changed from new to contacted", "timestamp": 1751215600000, "authorName": "Alex Rivera" },
    { "type": "note", "content": "Called back — wants an estimate Thursday morning.", "timestamp": 1751215500000, "authorName": "Alex Rivera" },
    { "type": "lead_created", "content": "New lead from website form", "timestamp": 1751212345678, "authorName": "System" }
  ],
  "journey": {
    "sessionId": "b3f09c41-77aa-4c1e-9f2d-2f4e8d1c6a55",
    "visitorId": "9a1c3f7e-d2b8-4e06-8f31-5c7a2e9d4b10",
    "startedAt": 1751212001000,
    "lastSeenAt": 1751212390000,
    "landingPage": "/services/ac-repair",
    "landingUrl": "https://www.example-hvac.com/services/ac-repair?utm_source=google&utm_medium=cpc",
    "referrer": "https://www.google.com/",
    "channel": "ppc",
    "sourceLabel": "Google Ads",
    "utmSource": "google",
    "utmMedium": "cpc",
    "utmCampaign": "summer-ac-repair",
    "utmTerm": "ac repair near me",
    "utmContent": "ad-variant-b",
    "gclid": "EAIaIQobChMI8p2c4vHq9gIVxxWtBh2QkQAxEAAYASAAEgL",
    "device": { "type": "mobile", "browser": "Safari", "browserVersion": "18.5", "os": "iOS", "osVersion": "18.5" },
    "geo": { "country": "US", "region": "TX", "city": "Austin" },
    "isReturning": true,
    "visitNumber": 3,
    "pageviews": [
      { "path": "/services/ac-repair", "title": "AC Repair", "timestamp": 1751212001200, "durationMs": 42000 },
      { "path": "/reviews", "title": "Reviews", "timestamp": 1751212043200, "durationMs": 28000 },
      { "path": "/contact", "title": "Contact Us", "timestamp": 1751212071200, "durationMs": 61000 }
    ],
    "events": [
      { "type": "scroll_depth", "path": "/services/ac-repair", "timestamp": 1751212030000, "scrollDepth": 75 },
      { "type": "tel_click", "path": "/services/ac-repair", "timestamp": 1751212040000, "elementText": "(555) 123-4567" },
      { "type": "form_start", "path": "/contact", "timestamp": 1751212080000, "formName": "contact", "fieldName": "name" },
      { "type": "form_submit", "path": "/contact", "timestamp": 1751212335000, "formName": "contact" }
    ],
    "otherVisits": [
      { "startedAt": 1750901000000, "landingPage": "/", "channel": "organic", "referrer": "https://www.google.com/" },
      { "startedAt": 1751050000000, "landingPage": "/reviews", "channel": "direct" }
    ]
  }
}

Returns 404 when the lead doesn't exist or belongs to a different organization; 400 for a malformed id.

GET/analytics/leads

Aggregate lead acquisition for a time range: total plus breakdowns by resolved channel, source label, status, and day. Career-form job applications are excluded from the counts and reported separately.

Request

Parameter Type Description
since number Range start, Unix timestamp in milliseconds. Default: 30 days ago.
until number Range end, Unix timestamp in milliseconds. Default: now.
# Default range (last 30 days)
curl -s https://api.persilio.com/api/v1/analytics/leads \
  -H "Authorization: Bearer psk_live_…"

# Explicit range — June 2026 (ms timestamps)
curl -s "https://api.persilio.com/api/v1/analytics/leads?since=1780272000000&until=1782864000000" \
  -H "Authorization: Bearer psk_live_…"

# Tip: build ms timestamps from a date
date -d "2026-06-01" +%s000        # linux
python3 -c "import datetime; print(int(datetime.datetime(2026,6,1).timestamp()*1000))"

Response

Field Type Description
range object The since/until range actually used (Unix ms).
total number Sales leads in range (job applications excluded).
jobApplicationsExcluded number Career-form submissions in range, excluded from all counts.
byChannel object Lead counts keyed by resolved channel. Missing attribution groups under "(none)".
bySource object Lead counts keyed by source label.
byStatus object Lead counts keyed by current status.
byDay object[] Per-day counts: { date: "YYYY-MM-DD" (UTC), count }.
truncated boolean True when the range hit the per-request scan cap — narrow the window.
{
  "range": { "since": 1780272000000, "until": 1782864000000 },
  "total": 42,
  "jobApplicationsExcluded": 3,
  "byChannel": { "organic": 18, "ppc": 12, "direct": 7, "referral": 3, "ai": 2 },
  "bySource": { "AC Repair": 21, "Furnace Install": 11, "(none)": 10 },
  "byStatus": { "new": 9, "contacted": 14, "scheduled": 8, "closed": 7, "lost": 4 },
  "byDay": [
    { "date": "2026-06-01", "count": 2 },
    { "date": "2026-06-02", "count": 1 }
  ],
  "truncated": false
}

GET/analytics/traffic

Website traffic from Persilio's visitor tracking for a time range: session and pageview totals, conversion and returning-visitor counts, and breakdowns by channel, device, country, and day, plus top referrers, landing pages, and pages.

Request

Parameter Type Description
since number Range start, Unix timestamp in milliseconds. Default: 30 days ago.
until number Range end, Unix timestamp in milliseconds. Default: now.
curl -s "https://api.persilio.com/api/v1/analytics/traffic?since=1780272000000&until=1782864000000" \
  -H "Authorization: Bearer psk_live_…"

Response

Field Type Description
range object The since/until range actually used (Unix ms).
sessions number Tracked sessions in range.
pageviews number Tracked pageviews in range.
returningSessions number Sessions from visitors with prior visits.
sessionsWithLead number Sessions that converted into a lead.
byChannel object Session counts keyed by resolved channel.
byDevice object Session counts keyed by device type.
byCountry object[] Top 10 countries: { key, count }.
byDay object[] Per-day session counts: { date: "YYYY-MM-DD" (UTC), count }.
topReferrers object[] Top 10 referrers: { key, count }.
topLandingPages object[] Top 10 landing pages: { key, count }.
topPages object[] Top 15 pages by views: { key, count }.
truncated boolean True when the range hit the per-request scan cap — narrow the window.
{
  "range": { "since": 1780272000000, "until": 1782864000000 },
  "sessions": 4064,
  "pageviews": 2905,
  "returningSessions": 312,
  "sessionsWithLead": 6,
  "byChannel": { "direct": 3536, "organic": 441, "referral": 69, "ai": 11, "social": 7 },
  "byDevice": { "mobile": 2983, "desktop": 1080, "tablet": 1 },
  "byCountry": [
    { "key": "US", "count": 3901 },
    { "key": "CA", "count": 82 }
  ],
  "byDay": [
    { "date": "2026-06-01", "count": 141 },
    { "date": "2026-06-02", "count": 129 }
  ],
  "topReferrers": [
    { "key": "https://www.google.com/", "count": 431 },
    { "key": "https://chatgpt.com/", "count": 6 }
  ],
  "topLandingPages": [
    { "key": "/", "count": 1512 },
    { "key": "/services/ac-repair", "count": 244 }
  ],
  "topPages": [
    { "key": "/", "count": 794 },
    { "key": "/contact", "count": 248 }
  ],
  "truncated": false
}

Object reference

The lead object

Returned by GET /leads (in the leads array) and GET /leads/{id} (top level). Fields marked with ? are omitted when they have no value.

Field Type Description
id string Unique lead id. Use with GET /leads/{id}.
createdAt number When the lead record was created (Unix ms).
cameInAt number When the lead actually came in (Unix ms). Differs from createdAt for imported/backdated leads.
name string The lead's full name.
email string? Email address, when provided.
phone string? Phone number, when provided.
message string? The message submitted with the form or captured from the call.
status string Current pipeline status. See Attribution values for all statuses.
leadKind string? Set to job_application for career-form submissions; absent for sales leads.
source string How the lead entered Persilio: website_form, manual, organic_call, or ppc_call.
sourceLabel string? Human-set label. For website_form leads this is usually the service the visitor selected.
channel string? Persilio's resolved first-touch acquisition channel. See Attribution values.
referrer string? The referring URL from the visitor session that produced the lead.
landingPage string? First page of the visit that produced the lead.
sourceUrl string? URL of the page the lead was submitted from.
utmSource string? utm_source from the first touch.
utmMedium string? utm_medium from the first touch.
utmCampaign string? utm_campaign from the first touch.
utmTerm string? utm_term from the first touch.
utmContent string? utm_content from the first touch.
gclid string? Google Ads click id, when the visit came from a Google ad.
fbclid string? Meta click id, when the visit came from a Facebook/Instagram ad.
msclkid string? Microsoft Ads click id.
sessionId string? Tracked browser session that produced the lead. Expanded in the journey object.
visitorId string? Persistent visitor id across that browser’s sessions.
contactId string? The contact (person) this lead belongs to.
pipelineStage string? Stage within the configured sales pipeline.
lostReason string? Why the lead was marked lost, when status is lost.
notes string[]? Free-form notes added by your team.
lastActivityAt number? Timestamp of the most recent activity on the lead (Unix ms).

The journey object

Present on GET /leads/{id} when the lead came through a website running Persilio tracking. It is the session that produced the lead — first-touch attribution, the page-by-page path, interaction events, device and location, and the visitor's other visits. Absent for manual leads and calls without a tracked session.

Field Type Description
sessionId string The tracked session id.
visitorId string Persistent visitor id.
startedAt number Session start (Unix ms).
lastSeenAt number Last activity in the session (Unix ms).
landingPage string First page of the session.
landingUrl string? Full landing URL including query string.
referrer string? Document referrer on the first page.
channel string? Resolved channel for this session.
sourceLabel string? Human-readable source, e.g. "Google Ads".
utmSource … utmContent string? The five UTM parameters captured on the first page.
gclid / fbclid / msclkid string? Ad click ids captured on the first page.
device object type (mobile | tablet | desktop), browser, browserVersion, os, osVersion.
geo object country, region, city — resolved at the network edge. Fields are omitted when unknown.
isReturning boolean? True when this visitor had prior sessions on your site.
visitNumber number? Which visit this was for the visitor (1 = first).
pageviews object[] Chronological pages viewed: path, title, timestamp, durationMs (time on page; absent for the final page).
events object[] Chronological interaction events: type, path, timestamp, plus elementText, formName, fieldName, scrollDepth, or value depending on type.
otherVisits object[] Up to 10 other sessions by the same visitor: startedAt, landingPage, channel, referrer.

Contact & activity objects

Also on GET /leads/{id}: contact is the person this opportunity belongs to (leads are opportunities; contacts are people — one contact can have many leads). activities is the newest-first audit trail of what your team did with the lead.

contact

Field Type Description
id string Contact id.
name string Full display name.
firstName / lastName string? Split name parts, when known.
email string? Email address.
phone string? Phone number.
company string? Company, when known.

activities[]

Field Type Description
type string Activity kind, e.g. lead_created, status_change, note, email_sent, call.
content string Human-readable description of what happened.
timestamp number When it happened (Unix ms).
authorName string? Team member (or system) that performed the action.

Attribution values

channel

Persilio's resolved acquisition channel, computed first-touch from the visitor's original session — not the last click before the form submit. Raw signals (referrer, UTMs, click IDs) are always included alongside it so you can apply your own model.

organic Unpaid search — Google, Bing, DuckDuckGo, etc.
ppc Paid ads — a gclid/msclkid was present or utm_medium indicated cpc/paid.
social Social networks — Facebook, Instagram, LinkedIn, TikTok, YouTube, etc.
referral Another website linked to you.
direct No referrer — typed URL, bookmark, or an app that strips referrers.
ai AI assistants — ChatGPT, Perplexity, Claude, Copilot, Gemini.

status

Lead statuses, in rough pipeline order:

newcontactedcontacted_calledcontacted_emailedcontacted_voicemailestimatedscheduledclosedlostdeclinedspam

source

How the lead entered Persilio: website_form (submitted a tracked form), organic_call / ppc_call (call tracking), or manual (entered by your team).

Journey event types

form_start Visitor focused a form field (fieldName says which).
form_submit Visitor submitted a form (formName).
tel_click Tapped/clicked a phone number link (elementText).
click Clicked a tracked element (elementId / elementText).
outbound_click Clicked a link leaving your site (value = destination).
scroll_depth Scrolled past a depth threshold (scrollDepth = percent).
rage_click Rapid repeated clicks — a frustration signal.
copy Copied text on the page.
video_play Started a video.
error A JavaScript error occurred on the page.

Website CMS

Organizations with a Persilio-managed website can read and edit its pages through the API — the same content the dashboard's website editor manages. Every CMS route requires the cms scope (or an unscoped key). Edits always land in a page's draft; nothing changes on the live site until a publish.

The block model

A page is an ordered list of blocks: { id, kind, props, hidden? }. The block kinds a site supports — and the exact fields each kind's props accepts — are defined per site and returned as blockDefs by GET /cms/sites and on every page response. Writes are validated against those definitions, and validation failures come back as readable 400 messages naming the block and field.

# A page's draft, abridged
{
  "id": "…", "slug": "index", "path": "/", "title": "Home",
  "blocks": [
    { "id": "1f3…", "kind": "hero",
      "props": { "heading": "Injured? We can help.", "sub": "…" } },
    { "id": "9ab…", "kind": "faq",
      "props": { "items": [ { "q": "…", "a": "…" } ] } }
  ],
  "blockDefs": [ …the schema each kind accepts… ]
}

Editing

For a single change, prefer the block-level operation — it can't accidentally drop content:

# Change one block's heading
curl -s -X POST "https://api.persilio.com/api/v1/cms/pages/{pageId}/blocks" \
  -H "Authorization: Bearer psk_live_…" -H "Content-Type: application/json" \
  -d '{ "action": "edit", "blockId": "1f3…",
        "props": { "heading": "New headline" } }'

# action: "add"    → { kind, props?, afterBlockId? }
# action: "remove" → { blockId }

POST /cms/pages/{id} replaces the draft wholesale (title, SEO, and/or the full blocks array — use it for reorders). If the submitted array would drop existing block ids, the call is rejected unless allowRemovals: true — so partial payloads can never silently delete content.

Publishing

POST /cms/publish promotes every page with draft changes (narrow with pageIds), commits the site's content to its GitHub repository, and triggers a redeploy. It returns 202 with a publish id; poll GET /cms/publishes/{id} for progress. Only one publish runs at a time per site — a second request returns 400 until the first resolves.

Publish status Meaning
queuedAccepted; drafts promoted, commit pending.
committingWriting content.json to the site's GitHub repo.
committedCommit landed (commitSha/commitUrl set); waiting for the build.
buildingThe site's deploy workflow is running.
deployedLive. Terminal.
failedSomething broke — error says what. Terminal; fix and publish again.

Every publish is attributed (dashboard user, API key name, or MCP) in the page's version history, and the git history of the site repository carries a full diff of every publish.

Errors

Errors return a JSON body with the shape { "error": { "message", "status" } }.

Status Meaning
400Malformed request — e.g. an invalid lead id.
401Missing, malformed, or revoked API key.
404Resource not found (or belongs to a different organization).
429Rate limit exceeded — honor the Retry-After header.

Analytics responses additionally include a truncated flag when a very large range hit the per-request scan cap — narrow the since/until window if you see it set.

MCP server for AI agents

Persilio ships a hosted Model Context Protocol server, so AI agents and assistants can work with your Persilio data directly, with no glue code: read leads and analytics and, where a key allows it, edit your website and training courses. It speaks Streamable HTTP and accepts two forms of authentication: the same API keys as the REST API (as a bearer header), or the built-in OAuth flow for clients that can't send headers — which is how claude.ai custom connectors work.

https://api.persilio.com/mcp

Available tools

Each tool wraps the corresponding REST endpoint, so the parameters and response shapes documented above apply directly.

Tool Description
get_org_info Basic info about the connected organization.
get_setup_status What the organization has set up (payments, courses, attendance, email, phone, Google) and what this key can reach — so an assistant knows what it can really do.
get_dashboard_summary Leads this month, active jobs, invoiced amount, upcoming bookings.
list_leads List/filter leads with full attribution; same parameters as GET /leads.
get_lead Full detail for one lead including the visitor journey.
get_lead_analytics Lead totals + breakdowns for a time range; same shape as GET /analytics/leads.
get_traffic_analytics Sessions, pageviews, channels, top pages; same shape as GET /analytics/traffic.
cms_list_sites The org’s CMS-managed websites + each site’s block vocabulary (cms scope).
cms_list_pages Pages of the website with draft/published state.
cms_get_page One page’s draft blocks plus the exact field schema each block accepts.
cms_create_page Create a draft page, optionally from a template.
cms_edit_block Change one block’s props by id — the reliable single-edit path.
cms_add_block Add a block of a supported kind at a position.
cms_remove_block Remove a block by id.
cms_update_page Replace a page’s draft (title, SEO, full block array) with block-loss protection.
cms_publish_site Promote drafts, commit to GitHub, redeploy the site.
cms_get_publish_status Watch a publish: committed → building → deployed / failed.
courses_list Training courses with module and lesson counts, published vs draft (courses scope).
courses_get A course’s outline: modules, sub-modules and lessons with their status.
courses_search Find lessons by what they say — every title and body, with snippets around each match.
courses_get_lesson One lesson in full: ordered blocks (text, callout, image, video, quiz) plus a plain-text reading.
courses_replace_text Find-and-replace inside a lesson. Matches the words a reader sees and rewrites only what changes, so formatting survives.
courses_update_block Replace one block by id.
courses_insert_blocks Add blocks at the start, end, an index, or after a block.
courses_remove_block Remove one block by id.
courses_create_lesson Create a lesson from blocks (a draft until published).
courses_update_lesson Title, description, draft/published, signature requirement, thumbnail, attachments, or move to another module.
courses_delete_lesson Delete a lesson (refused once it has signed acknowledgements).
courses_create / courses_update / courses_delete Create a course, change its details or publish state, or delete it.
courses_create_module / courses_update_module / courses_delete_module Manage modules and sub-modules (one level of nesting).
courses_reorder_modules / courses_reorder_lessons Set the order of sibling modules, or of the lessons in a module.
courses_list_media The Documents library, newest first: images, videos and files with the id a block references.
courses_request_upload Get a link the person opens to drop files from their computer into the library — how a photo attached to a chat gets into a lesson.
courses_get_uploads What arrived through an upload link, with the id each file is referenced by.
courses_import_media Fetch a file from a public URL into the library (50 MB).
courses_progress Who has taken a course: lessons completed, time spent, signed acknowledgements.
search_contacts Search the contact book by name, email, company, or phone digits (contacts scope).
get_contact One contact plus every quote/invoice linked to them.
create_contact Create a contact; dedupes on email/phone.
update_contact Patch contact fields, including lifecycle.
list_quotes Quotes and invoices with totals, status, client, dashboard URL (billing scope).
get_quote One quote in full: line items with term settings, written terms, e-signature URL, and the agreement text as the PDF renders it.
preview_agreement Render the Service Agreement text for line items + provisions before saving.
create_quote Create a draft quote or invoice (amounts in cents); quotes get an e-signature link.
update_quote Edit an unpaid quote; editing an unsigned quote rotates its signing link.
set_quote_status draft | created | sent | signed | canceled | voided.
get_quote_sign_link The hosted e-signature URL (created on demand). The server never emails clients.
list_contract_term_templates Named written-terms templates and the org default terms.
save_contract_term_template Create or update a terms template (adminOnly locks it to sysadmins).
delete_contract_term_template Delete a terms template.
set_default_contract_terms Replace the org’s default written terms.
list_quote_templates Saved line-item bundles from the quote editor.
save_quote_template Save a new quote template.
delete_quote_template Move a quote template to the trash.
seo_list_clients Clients enrolled in the SEO program with this month’s progress (Savo Group staff keys, seo scope).
seo_get_month A client’s Monthly Work board for a month: tasks, status, assignee, proof links.
seo_staff Team members who can be assigned SEO tasks.
seo_create_task Add a task (optionally already done) to a client’s month.
seo_update_task Change status, notes, proof link, assignee, due date, priority.
seo_comment_on_task Comment on a task.
seo_activity Who changed what on the SEO program, and when.

Connect from claude.ai or Claude Desktop (custom connector)

Claude's custom connectors authenticate with OAuth — the Persilio MCP server implements the full flow, so no configuration files are needed:

  1. In claude.ai (or Claude Desktop), open Settings → Connectors → Add custom connector.
  2. Name it Persilio and enter the server URL: https://api.persilio.com/mcp
  3. Leave the OAuth Client ID and Secret fields under Advanced settings empty — Claude registers itself automatically.
  4. Click Add, then Connect. A Persilio authorization page opens.
  5. Paste an API key from Dashboard → Settings → API access and click Authorize. You're redirected back to Claude with the connector active.

The connector receives a token tied to the API key you pasted — revoking that key in your dashboard disconnects the connector immediately. It can do exactly what that key allows and nothing more: a key is limited to the areas you tick when you create it (and to the areas its holder can open in the dashboard), and it only ever reaches your organization. Claude lists the tools in two groups — read-only, and tools that change things — so you can let it read freely and still be asked before every edit.

Edit your training courses from Claude

To let Claude maintain your courses, create a key with only the Courses area ticked and connect it as above. Then talk to it the way you would to an editor:

  • "Find every lesson that mentions our 60-day probation period and change it to 90 days."
  • "Rewrite the Reporting an Injury on the Job lesson at a fifth-grade reading level. Show me before you save."
  • "Add a three-question quiz to the end of Customer Communication Standards."
  • "Move Vehicle Inspection Checklist into the Safety module and set it back to draft."
  • "Here's a photo of the correct PPE setup — put it at the top of the Job Site Safety lesson with a caption."

Edits are live as soon as Claude makes them, and new lessons stay drafts until you publish them. A photo dropped into the chat can't be read by a connector, so Claude hands you an upload link instead: open it, drop the file, tell Claude it's there, and it places the image in the lesson. The file is kept under Documents → Course Media.

When new tools are added to the connector: Claude keeps a copy of a connector's tool list. If a tool described here is missing in your chat, open Settings → Connectors, click Persilio, and choose Refresh tools list. If that doesn't pick it up, add the connector again with https://api.persilio.com/mcp?v=2 as the URL.

Connect from Claude Code

claude mcp add --transport http persilio https://api.persilio.com/mcp \
  --header "Authorization: Bearer psk_live_…"

Connect from Cursor / other MCP clients

Clients that support custom headers can send the API key directly. Clients that only support OAuth (like claude.ai above) work through the built-in flow automatically. The generic header-based configuration shape:

{
  "mcpServers": {
    "persilio": {
      "url": "https://api.persilio.com/mcp",
      "headers": {
        "Authorization": "Bearer psk_live_…"
      }
    }
  }
}

Once connected, ask things like "how many organic leads did we get this month?", "show me the full journey behind our newest lead", or "which landing pages are converting?" — the agent pulls live data through the tools above.

Ready to build?

Create an API key in your dashboard and make your first request in under a minute.

Open dashboard settings